mirror of
https://github.com/github/spec-kit.git
synced 2026-04-02 18:53:09 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8ccb0609d | ||
|
|
663d679f3b | ||
|
|
b1832c9477 | ||
|
|
a858c1d6da | ||
|
|
d9ce7c1fc0 | ||
|
|
4f9d966beb | ||
|
|
b44ffc0101 | ||
|
|
8e14ab1935 | ||
|
|
0945df9ec8 | ||
|
|
ea60efe2fa | ||
|
|
97b9f0f00d | ||
|
|
4df6d963dc | ||
|
|
682ffbfc0d | ||
|
|
b606b38512 | ||
|
|
255371d367 | ||
|
|
3113b72d6f | ||
|
|
3899dcc0d4 | ||
|
|
b8335a532c | ||
|
|
cb16412f88 | ||
|
|
804cd10c71 | ||
|
|
4dff63a84e | ||
|
|
40ecd44ada | ||
|
|
b19a7eedfa | ||
|
|
9cb3f3d1ad | ||
|
|
f8da535d71 | ||
|
|
edaa5a7ff1 | ||
|
|
5be705e414 | ||
|
|
796b4f47c4 | ||
|
|
6b1f45c50c | ||
|
|
8778c26dcf | ||
|
|
41d1f4b0ac | ||
|
|
9c2481fd67 | ||
|
|
8520241dfe | ||
|
|
362868a342 | ||
|
|
d7206126e0 | ||
|
|
b22f381c0d | ||
|
|
ccc44dd00a | ||
|
|
2c2fea8783 | ||
|
|
4b4bd735a3 |
@@ -12,7 +12,7 @@ body:
|
|||||||
- Review the [Extension Publishing Guide](https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-PUBLISHING-GUIDE.md)
|
- 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
|
- Ensure your extension has a valid `extension.yml` manifest
|
||||||
- Create a GitHub release with a version tag (e.g., v1.0.0)
|
- Create a GitHub release with a version tag (e.g., v1.0.0)
|
||||||
- Test installation: `specify extension add --from <your-release-url>`
|
- Test installation: `specify extension add <extension-name> --from <your-release-url>`
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: extension-id
|
id: extension-id
|
||||||
@@ -229,7 +229,7 @@ body:
|
|||||||
placeholder: |
|
placeholder: |
|
||||||
```bash
|
```bash
|
||||||
# Install extension
|
# Install extension
|
||||||
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
|
|
||||||
# Use a command
|
# Use a command
|
||||||
/speckit.your-extension.command-name arg1 arg2
|
/speckit.your-extension.command-name arg1 arg2
|
||||||
|
|||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -64,5 +64,5 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v5
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Run markdownlint-cli2
|
- name: Run markdownlint-cli2
|
||||||
uses: DavidAnson/markdownlint-cli2-action@v19
|
uses: DavidAnson/markdownlint-cli2-action@ce4853d43830c74c1753b39f3cf40f71c2031eb9 # v23
|
||||||
with:
|
with:
|
||||||
globs: |
|
globs: |
|
||||||
'**/*.md'
|
'**/*.md'
|
||||||
|
|||||||
45
.github/workflows/release-trigger.yml
vendored
45
.github/workflows/release-trigger.yml
vendored
@@ -100,18 +100,16 @@ jobs:
|
|||||||
COMMITS="- Initial release"
|
COMMITS="- Initial release"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create new changelog entry
|
# Create new changelog entry — insert after the marker comment
|
||||||
{
|
NEW_ENTRY=$(printf '%s\n' \
|
||||||
head -n 8 CHANGELOG.md
|
"" \
|
||||||
echo ""
|
"## [${{ steps.version.outputs.version }}] - $DATE" \
|
||||||
echo "## [${{ steps.version.outputs.version }}] - $DATE"
|
"" \
|
||||||
echo ""
|
"### Changed" \
|
||||||
echo "### Changes"
|
"" \
|
||||||
echo ""
|
"$COMMITS")
|
||||||
echo "$COMMITS"
|
|
||||||
echo ""
|
awk -v entry="$NEW_ENTRY" '/<!-- insert new changelog below this comment -->/ { print; print entry; next } {print}' CHANGELOG.md > CHANGELOG.md.tmp
|
||||||
tail -n +9 CHANGELOG.md
|
|
||||||
} > CHANGELOG.md.tmp
|
|
||||||
mv CHANGELOG.md.tmp CHANGELOG.md
|
mv CHANGELOG.md.tmp CHANGELOG.md
|
||||||
|
|
||||||
echo "✅ Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
|
echo "✅ Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
|
||||||
@@ -141,6 +139,22 @@ jobs:
|
|||||||
git push origin "${{ steps.version.outputs.tag }}"
|
git push origin "${{ steps.version.outputs.tag }}"
|
||||||
echo "Branch ${{ env.branch }} and tag ${{ steps.version.outputs.tag }} pushed"
|
echo "Branch ${{ env.branch }} and tag ${{ steps.version.outputs.tag }} pushed"
|
||||||
|
|
||||||
|
- name: Bump to dev version
|
||||||
|
id: dev_version
|
||||||
|
run: |
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "${{ steps.version.outputs.version }}"
|
||||||
|
NEXT_DEV="$MAJOR.$MINOR.$((PATCH + 1)).dev0"
|
||||||
|
echo "dev_version=$NEXT_DEV" >> $GITHUB_OUTPUT
|
||||||
|
sed -i "s/version = \".*\"/version = \"$NEXT_DEV\"/" pyproject.toml
|
||||||
|
git add pyproject.toml
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No dev version changes to commit"
|
||||||
|
else
|
||||||
|
git commit -m "chore: begin $NEXT_DEV development"
|
||||||
|
git push origin "${{ env.branch }}"
|
||||||
|
echo "Bumped to dev version $NEXT_DEV"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||||
@@ -148,16 +162,17 @@ jobs:
|
|||||||
gh pr create \
|
gh pr create \
|
||||||
--base main \
|
--base main \
|
||||||
--head "${{ env.branch }}" \
|
--head "${{ env.branch }}" \
|
||||||
--title "chore: bump version to ${{ steps.version.outputs.version }}" \
|
--title "chore: release ${{ steps.version.outputs.version }}, begin ${{ steps.dev_version.outputs.dev_version }} development" \
|
||||||
--body "Automated version bump to ${{ steps.version.outputs.version }}.
|
--body "Automated release of ${{ steps.version.outputs.version }}.
|
||||||
|
|
||||||
This PR was created by the Release Trigger workflow. The git tag \`${{ steps.version.outputs.tag }}\` has already been pushed and the release artifacts are being built.
|
This PR was created by the Release Trigger workflow. The git tag \`${{ steps.version.outputs.tag }}\` has already been pushed and the release artifacts are being built.
|
||||||
|
|
||||||
Merge this PR to record the version bump and changelog update on \`main\`."
|
Merging this PR will set \`main\` to \`${{ steps.dev_version.outputs.dev_version }}\` so that development installs are clearly marked as pre-release."
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
|
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
|
||||||
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
|
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
|
||||||
|
echo "✅ Dev version set to ${{ steps.dev_version.outputs.dev_version }}"
|
||||||
echo "✅ PR opened to merge version bump into main"
|
echo "✅ PR opened to merge version bump into main"
|
||||||
echo "🚀 Release workflow is building artifacts from the tag"
|
echo "🚀 Release workflow is building artifacts from the tag"
|
||||||
|
|||||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -27,35 +27,63 @@ jobs:
|
|||||||
- name: Check if release already exists
|
- name: Check if release already exists
|
||||||
id: check_release
|
id: check_release
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/check-release-exists.sh
|
VERSION="${{ steps.version.outputs.tag }}"
|
||||||
.github/workflows/scripts/check-release-exists.sh ${{ steps.version.outputs.tag }}
|
if gh release view "$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $VERSION already exists, skipping..."
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $VERSION does not exist, proceeding..."
|
||||||
|
fi
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create release package variants
|
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
|
||||||
run: |
|
|
||||||
chmod +x .github/workflows/scripts/create-release-packages.sh
|
|
||||||
.github/workflows/scripts/create-release-packages.sh ${{ steps.version.outputs.tag }}
|
|
||||||
|
|
||||||
- name: Generate release notes
|
- name: Generate release notes
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
id: release_notes
|
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/generate-release-notes.sh
|
VERSION="${{ steps.version.outputs.tag }}"
|
||||||
# Get the previous tag for changelog generation
|
VERSION_NO_V=${VERSION#v}
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${{ steps.version.outputs.tag }}^ 2>/dev/null || echo "")
|
|
||||||
# Default to v0.0.0 if no previous tag is found (e.g., first release)
|
# Find previous tag
|
||||||
|
PREVIOUS_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${VERSION}$" | head -n 1)
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
PREVIOUS_TAG="v0.0.0"
|
PREVIOUS_TAG=""
|
||||||
fi
|
fi
|
||||||
.github/workflows/scripts/generate-release-notes.sh ${{ steps.version.outputs.tag }} "$PREVIOUS_TAG"
|
|
||||||
|
# Get commits since previous tag
|
||||||
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
|
if [ "$COMMIT_COUNT" -gt 20 ]; then
|
||||||
|
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges HEAD~20..HEAD)
|
||||||
|
else
|
||||||
|
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges)
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges "$PREVIOUS_TAG"..HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > release_notes.md << NOTES_EOF
|
||||||
|
## Install
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@${VERSION}
|
||||||
|
specify init my-project
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
NOTES_EOF
|
||||||
|
|
||||||
|
echo "## What's Changed" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
echo "$COMMITS" >> release_notes.md
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
chmod +x .github/workflows/scripts/create-github-release.sh
|
VERSION="${{ steps.version.outputs.tag }}"
|
||||||
.github/workflows/scripts/create-github-release.sh ${{ steps.version.outputs.tag }}
|
VERSION_NO_V=${VERSION#v}
|
||||||
|
gh release create "$VERSION" \
|
||||||
|
--title "Spec Kit - $VERSION_NO_V" \
|
||||||
|
--notes-file release_notes.md
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# check-release-exists.sh
|
|
||||||
# Check if a GitHub release already exists for the given version
|
|
||||||
# Usage: check-release-exists.sh <version>
|
|
||||||
|
|
||||||
if [[ $# -ne 1 ]]; then
|
|
||||||
echo "Usage: $0 <version>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION="$1"
|
|
||||||
|
|
||||||
if gh release view "$VERSION" >/dev/null 2>&1; then
|
|
||||||
echo "exists=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Release $VERSION already exists, skipping..."
|
|
||||||
else
|
|
||||||
echo "exists=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "Release $VERSION does not exist, proceeding..."
|
|
||||||
fi
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# create-github-release.sh
|
|
||||||
# Create a GitHub release with all template zip files
|
|
||||||
# Usage: create-github-release.sh <version>
|
|
||||||
|
|
||||||
if [[ $# -ne 1 ]]; then
|
|
||||||
echo "Usage: $0 <version>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION="$1"
|
|
||||||
|
|
||||||
# Remove 'v' prefix from version for release title
|
|
||||||
VERSION_NO_V=${VERSION#v}
|
|
||||||
|
|
||||||
gh release create "$VERSION" \
|
|
||||||
.genreleases/spec-kit-template-copilot-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-copilot-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-claude-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-claude-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-gemini-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-gemini-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-cursor-agent-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-cursor-agent-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-opencode-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-opencode-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-qwen-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-qwen-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-windsurf-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-windsurf-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-junie-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-junie-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-codex-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-codex-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kilocode-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kilocode-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-auggie-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-auggie-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-roo-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-roo-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-qodercli-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-qodercli-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-amp-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-tabnine-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-tabnine-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kiro-cli-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kiro-cli-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-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-vibe-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-vibe-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kimi-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-kimi-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-trae-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-trae-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-pi-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-pi-ps-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-iflow-sh-"$VERSION".zip \
|
|
||||||
.genreleases/spec-kit-template-iflow-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" \
|
|
||||||
--notes-file release_notes.md
|
|
||||||
@@ -1,561 +0,0 @@
|
|||||||
#!/usr/bin/env pwsh
|
|
||||||
#requires -Version 7.0
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Build Spec Kit template release archives for each supported AI assistant and script type.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
create-release-packages.ps1 (workflow-local)
|
|
||||||
Build Spec Kit template release archives for each supported AI assistant and script type.
|
|
||||||
|
|
||||||
.PARAMETER Version
|
|
||||||
Version string with leading 'v' (e.g., v0.2.0)
|
|
||||||
|
|
||||||
.PARAMETER Agents
|
|
||||||
Comma or space separated subset of agents to build (default: all)
|
|
||||||
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, junie, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, trae, pi, iflow, generic
|
|
||||||
|
|
||||||
.PARAMETER Scripts
|
|
||||||
Comma or space separated subset of script types to build (default: both)
|
|
||||||
Valid scripts: sh, ps
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\create-release-packages.ps1 -Version v0.2.0
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude,copilot -Scripts sh
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude -Scripts ps
|
|
||||||
#>
|
|
||||||
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true, Position=0)]
|
|
||||||
[string]$Version,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Agents = "",
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Scripts = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
# Validate version format
|
|
||||||
if ($Version -notmatch '^v\d+\.\d+\.\d+$') {
|
|
||||||
Write-Error "Version must look like v0.0.0"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Building release packages for $Version"
|
|
||||||
|
|
||||||
# Create and use .genreleases directory for all build artifacts
|
|
||||||
$GenReleasesDir = ".genreleases"
|
|
||||||
if (Test-Path $GenReleasesDir) {
|
|
||||||
Remove-Item -Path $GenReleasesDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
New-Item -ItemType Directory -Path $GenReleasesDir -Force | Out-Null
|
|
||||||
|
|
||||||
function Rewrite-Paths {
|
|
||||||
param([string]$Content)
|
|
||||||
|
|
||||||
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
|
|
||||||
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
|
|
||||||
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
|
|
||||||
return $Content
|
|
||||||
}
|
|
||||||
|
|
||||||
function Generate-Commands {
|
|
||||||
param(
|
|
||||||
[string]$Agent,
|
|
||||||
[string]$Extension,
|
|
||||||
[string]$ArgFormat,
|
|
||||||
[string]$OutputDir,
|
|
||||||
[string]$ScriptVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
|
||||||
|
|
||||||
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
foreach ($template in $templates) {
|
|
||||||
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
|
||||||
|
|
||||||
# Read file content and normalize line endings
|
|
||||||
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
|
||||||
|
|
||||||
# Extract description from YAML frontmatter
|
|
||||||
$description = ""
|
|
||||||
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
|
||||||
$description = $matches[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract script command from YAML frontmatter
|
|
||||||
$scriptCommand = ""
|
|
||||||
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
|
||||||
$scriptCommand = $matches[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrEmpty($scriptCommand)) {
|
|
||||||
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
|
|
||||||
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract agent_script command from YAML frontmatter if present
|
|
||||||
$agentScriptCommand = ""
|
|
||||||
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
|
||||||
$agentScriptCommand = $matches[1].Trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Replace {SCRIPT} placeholder with the script command
|
|
||||||
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
|
||||||
|
|
||||||
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
|
||||||
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
|
||||||
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove the scripts: and agent_scripts: sections from frontmatter
|
|
||||||
$lines = $body -split "`n"
|
|
||||||
$outputLines = @()
|
|
||||||
$inFrontmatter = $false
|
|
||||||
$skipScripts = $false
|
|
||||||
$dashCount = 0
|
|
||||||
|
|
||||||
foreach ($line in $lines) {
|
|
||||||
if ($line -match '^---$') {
|
|
||||||
$outputLines += $line
|
|
||||||
$dashCount++
|
|
||||||
if ($dashCount -eq 1) {
|
|
||||||
$inFrontmatter = $true
|
|
||||||
} else {
|
|
||||||
$inFrontmatter = $false
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($inFrontmatter) {
|
|
||||||
if ($line -match '^(scripts|agent_scripts):$') {
|
|
||||||
$skipScripts = $true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ($line -match '^[a-zA-Z].*:' -and $skipScripts) {
|
|
||||||
$skipScripts = $false
|
|
||||||
}
|
|
||||||
if ($skipScripts -and $line -match '^\s+') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$outputLines += $line
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = $outputLines -join "`n"
|
|
||||||
|
|
||||||
# Apply other substitutions
|
|
||||||
$body = $body -replace '\{ARGS\}', $ArgFormat
|
|
||||||
$body = $body -replace '__AGENT__', $Agent
|
|
||||||
$body = Rewrite-Paths -Content $body
|
|
||||||
|
|
||||||
# Generate output file based on extension
|
|
||||||
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
|
|
||||||
|
|
||||||
switch ($Extension) {
|
|
||||||
'toml' {
|
|
||||||
$body = $body -replace '\\', '\\'
|
|
||||||
$output = "description = `"$description`"`n`nprompt = `"`"`"`n$body`n`"`"`""
|
|
||||||
Set-Content -Path $outputFile -Value $output -NoNewline
|
|
||||||
}
|
|
||||||
'md' {
|
|
||||||
Set-Content -Path $outputFile -Value $body -NoNewline
|
|
||||||
}
|
|
||||||
'agent.md' {
|
|
||||||
Set-Content -Path $outputFile -Value $body -NoNewline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Generate-CopilotPrompts {
|
|
||||||
param(
|
|
||||||
[string]$AgentsDir,
|
|
||||||
[string]$PromptsDir
|
|
||||||
)
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
|
|
||||||
|
|
||||||
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
foreach ($agentFile in $agentFiles) {
|
|
||||||
$basename = $agentFile.Name -replace '\.agent\.md$', ''
|
|
||||||
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
|
|
||||||
|
|
||||||
$content = @"
|
|
||||||
---
|
|
||||||
agent: $basename
|
|
||||||
---
|
|
||||||
"@
|
|
||||||
Set-Content -Path $promptFile -Value $content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create skills in <skills_dir>\<name>\SKILL.md format.
|
|
||||||
# Most agents use hyphenated names (e.g. speckit-plan); Kimi is the
|
|
||||||
# current dotted-name exception (e.g. speckit.plan).
|
|
||||||
#
|
|
||||||
# Technical debt note:
|
|
||||||
# Keep SKILL.md frontmatter aligned with `install_ai_skills()` and extension
|
|
||||||
# overrides (at minimum: name/description/compatibility/metadata.{author,source}).
|
|
||||||
function New-Skills {
|
|
||||||
param(
|
|
||||||
[string]$SkillsDir,
|
|
||||||
[string]$ScriptVariant,
|
|
||||||
[string]$AgentName,
|
|
||||||
[string]$Separator = '-'
|
|
||||||
)
|
|
||||||
|
|
||||||
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
foreach ($template in $templates) {
|
|
||||||
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
|
||||||
$skillName = "speckit${Separator}$name"
|
|
||||||
$skillDir = Join-Path $SkillsDir $skillName
|
|
||||||
New-Item -ItemType Directory -Force -Path $skillDir | Out-Null
|
|
||||||
|
|
||||||
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
|
||||||
|
|
||||||
# Extract description
|
|
||||||
$description = "Spec Kit: $name workflow"
|
|
||||||
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
|
||||||
$description = $matches[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract script command
|
|
||||||
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
|
||||||
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
|
||||||
$scriptCommand = $matches[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract agent_script command from frontmatter if present
|
|
||||||
$agentScriptCommand = ""
|
|
||||||
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
|
||||||
$agentScriptCommand = $matches[1].Trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
# Replace {SCRIPT}, strip scripts sections, rewrite paths
|
|
||||||
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
|
||||||
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
|
||||||
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
$lines = $body -split "`n"
|
|
||||||
$outputLines = @()
|
|
||||||
$inFrontmatter = $false
|
|
||||||
$skipScripts = $false
|
|
||||||
$dashCount = 0
|
|
||||||
|
|
||||||
foreach ($line in $lines) {
|
|
||||||
if ($line -match '^---$') {
|
|
||||||
$outputLines += $line
|
|
||||||
$dashCount++
|
|
||||||
$inFrontmatter = ($dashCount -eq 1)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ($inFrontmatter) {
|
|
||||||
if ($line -match '^(scripts|agent_scripts):$') { $skipScripts = $true; continue }
|
|
||||||
if ($line -match '^[a-zA-Z].*:' -and $skipScripts) { $skipScripts = $false }
|
|
||||||
if ($skipScripts -and $line -match '^\s+') { continue }
|
|
||||||
}
|
|
||||||
$outputLines += $line
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = $outputLines -join "`n"
|
|
||||||
$body = $body -replace '\{ARGS\}', '$ARGUMENTS'
|
|
||||||
$body = $body -replace '__AGENT__', $AgentName
|
|
||||||
$body = Rewrite-Paths -Content $body
|
|
||||||
|
|
||||||
# Strip existing frontmatter, keep only body
|
|
||||||
$templateBody = ""
|
|
||||||
$fmCount = 0
|
|
||||||
$inBody = $false
|
|
||||||
foreach ($line in ($body -split "`n")) {
|
|
||||||
if ($line -match '^---$') {
|
|
||||||
$fmCount++
|
|
||||||
if ($fmCount -eq 2) { $inBody = $true }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ($inBody) { $templateBody += "$line`n" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$skillContent = "---`nname: `"$skillName`"`ndescription: `"$description`"`ncompatibility: `"Requires spec-kit project structure with .specify/ directory`"`nmetadata:`n author: `"github-spec-kit`"`n source: `"templates/commands/$name.md`"`n---`n`n$templateBody"
|
|
||||||
Set-Content -Path (Join-Path $skillDir "SKILL.md") -Value $skillContent -NoNewline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-Variant {
|
|
||||||
param(
|
|
||||||
[string]$Agent,
|
|
||||||
[string]$Script
|
|
||||||
)
|
|
||||||
|
|
||||||
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
|
|
||||||
Write-Host "Building $Agent ($Script) package..."
|
|
||||||
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
|
|
||||||
|
|
||||||
# Copy base structure but filter scripts by variant
|
|
||||||
$specDir = Join-Path $baseDir ".specify"
|
|
||||||
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
|
|
||||||
|
|
||||||
# Copy memory directory
|
|
||||||
if (Test-Path "memory") {
|
|
||||||
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
|
|
||||||
Write-Host "Copied memory -> .specify"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Only copy the relevant script variant directory
|
|
||||||
if (Test-Path "scripts") {
|
|
||||||
$scriptsDestDir = Join-Path $specDir "scripts"
|
|
||||||
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
|
|
||||||
|
|
||||||
switch ($Script) {
|
|
||||||
'sh' {
|
|
||||||
if (Test-Path "scripts/bash") {
|
|
||||||
Copy-Item -Path "scripts/bash" -Destination $scriptsDestDir -Recurse -Force
|
|
||||||
Write-Host "Copied scripts/bash -> .specify/scripts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'ps' {
|
|
||||||
if (Test-Path "scripts/powershell") {
|
|
||||||
Copy-Item -Path "scripts/powershell" -Destination $scriptsDestDir -Recurse -Force
|
|
||||||
Write-Host "Copied scripts/powershell -> .specify/scripts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
|
||||||
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Copy templates (excluding commands directory and vscode-settings.json)
|
|
||||||
if (Test-Path "templates") {
|
|
||||||
$templatesDestDir = Join-Path $specDir "templates"
|
|
||||||
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
|
|
||||||
|
|
||||||
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
|
|
||||||
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
|
|
||||||
} | ForEach-Object {
|
|
||||||
$relativePath = $_.FullName.Substring((Resolve-Path "templates").Path.Length + 1)
|
|
||||||
$destFile = Join-Path $templatesDestDir $relativePath
|
|
||||||
$destFileDir = Split-Path $destFile -Parent
|
|
||||||
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
|
|
||||||
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
|
||||||
}
|
|
||||||
Write-Host "Copied templates -> .specify/templates"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate agent-specific command files
|
|
||||||
switch ($Agent) {
|
|
||||||
'claude' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".claude/commands"
|
|
||||||
Generate-Commands -Agent 'claude' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'gemini' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".gemini/commands"
|
|
||||||
Generate-Commands -Agent 'gemini' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
if (Test-Path "agent_templates/gemini/GEMINI.md") {
|
|
||||||
Copy-Item -Path "agent_templates/gemini/GEMINI.md" -Destination (Join-Path $baseDir "GEMINI.md")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'copilot' {
|
|
||||||
$agentsDir = Join-Path $baseDir ".github/agents"
|
|
||||||
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
|
||||||
|
|
||||||
$promptsDir = Join-Path $baseDir ".github/prompts"
|
|
||||||
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
|
||||||
|
|
||||||
$vscodeDir = Join-Path $baseDir ".vscode"
|
|
||||||
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
|
||||||
if (Test-Path "templates/vscode-settings.json") {
|
|
||||||
Copy-Item -Path "templates/vscode-settings.json" -Destination (Join-Path $vscodeDir "settings.json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'cursor-agent' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".cursor/commands"
|
|
||||||
Generate-Commands -Agent 'cursor-agent' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'qwen' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
|
||||||
Generate-Commands -Agent 'qwen' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
if (Test-Path "agent_templates/qwen/QWEN.md") {
|
|
||||||
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'opencode' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".opencode/command"
|
|
||||||
Generate-Commands -Agent 'opencode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'windsurf' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".windsurf/workflows"
|
|
||||||
Generate-Commands -Agent 'windsurf' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'junie' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".junie/commands"
|
|
||||||
Generate-Commands -Agent 'junie' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'codex' {
|
|
||||||
$skillsDir = Join-Path $baseDir ".agents/skills"
|
|
||||||
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
|
|
||||||
New-Skills -SkillsDir $skillsDir -ScriptVariant $Script -AgentName 'codex' -Separator '-'
|
|
||||||
}
|
|
||||||
'kilocode' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".kilocode/workflows"
|
|
||||||
Generate-Commands -Agent 'kilocode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'auggie' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".augment/commands"
|
|
||||||
Generate-Commands -Agent 'auggie' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'roo' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".roo/commands"
|
|
||||||
Generate-Commands -Agent 'roo' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'codebuddy' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".codebuddy/commands"
|
|
||||||
Generate-Commands -Agent 'codebuddy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'amp' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".agents/commands"
|
|
||||||
Generate-Commands -Agent 'amp' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'kiro-cli' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".kiro/prompts"
|
|
||||||
Generate-Commands -Agent 'kiro-cli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'bob' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".bob/commands"
|
|
||||||
Generate-Commands -Agent 'bob' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'qodercli' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".qoder/commands"
|
|
||||||
Generate-Commands -Agent 'qodercli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'shai' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".shai/commands"
|
|
||||||
Generate-Commands -Agent 'shai' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'tabnine' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".tabnine/agent/commands"
|
|
||||||
Generate-Commands -Agent 'tabnine' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
$tabnineTemplate = Join-Path 'agent_templates' 'tabnine/TABNINE.md'
|
|
||||||
if (Test-Path $tabnineTemplate) { Copy-Item $tabnineTemplate (Join-Path $baseDir 'TABNINE.md') }
|
|
||||||
}
|
|
||||||
'agy' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".agent/commands"
|
|
||||||
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'vibe' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".vibe/prompts"
|
|
||||||
Generate-Commands -Agent 'vibe' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'kimi' {
|
|
||||||
$skillsDir = Join-Path $baseDir ".kimi/skills"
|
|
||||||
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
|
|
||||||
New-Skills -SkillsDir $skillsDir -ScriptVariant $Script -AgentName 'kimi' -Separator '.'
|
|
||||||
}
|
|
||||||
'trae' {
|
|
||||||
$rulesDir = Join-Path $baseDir ".trae/rules"
|
|
||||||
New-Item -ItemType Directory -Force -Path $rulesDir | Out-Null
|
|
||||||
Generate-Commands -Agent 'trae' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $rulesDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'pi' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".pi/prompts"
|
|
||||||
Generate-Commands -Agent 'pi' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
|
||||||
}
|
|
||||||
'iflow' {
|
|
||||||
$cmdDir = Join-Path $baseDir ".iflow/commands"
|
|
||||||
Generate-Commands -Agent 'iflow' -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
|
|
||||||
}
|
|
||||||
default {
|
|
||||||
throw "Unsupported agent '$Agent'."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create zip archive
|
|
||||||
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
|
|
||||||
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
|
|
||||||
Write-Host "Created $zipFile"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define all agents and scripts
|
|
||||||
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'junie', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'trae', 'pi', 'iflow', 'generic')
|
|
||||||
$AllScripts = @('sh', 'ps')
|
|
||||||
|
|
||||||
function Normalize-List {
|
|
||||||
param([string]$Input)
|
|
||||||
|
|
||||||
if ([string]::IsNullOrEmpty($Input)) {
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
|
||||||
return $items
|
|
||||||
}
|
|
||||||
|
|
||||||
function Validate-Subset {
|
|
||||||
param(
|
|
||||||
[string]$Type,
|
|
||||||
[string[]]$Allowed,
|
|
||||||
[string[]]$Items
|
|
||||||
)
|
|
||||||
|
|
||||||
$ok = $true
|
|
||||||
foreach ($item in $Items) {
|
|
||||||
if ($item -notin $Allowed) {
|
|
||||||
Write-Error "Unknown $Type '$item' (allowed: $($Allowed -join ', '))"
|
|
||||||
$ok = $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $ok
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine agent list
|
|
||||||
if (-not [string]::IsNullOrEmpty($Agents)) {
|
|
||||||
$AgentList = Normalize-List -Input $Agents
|
|
||||||
if (-not (Validate-Subset -Type 'agent' -Allowed $AllAgents -Items $AgentList)) {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$AgentList = $AllAgents
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine script list
|
|
||||||
if (-not [string]::IsNullOrEmpty($Scripts)) {
|
|
||||||
$ScriptList = Normalize-List -Input $Scripts
|
|
||||||
if (-not (Validate-Subset -Type 'script' -Allowed $AllScripts -Items $ScriptList)) {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$ScriptList = $AllScripts
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Agents: $($AgentList -join ', ')"
|
|
||||||
Write-Host "Scripts: $($ScriptList -join ', ')"
|
|
||||||
|
|
||||||
# Build all variants
|
|
||||||
foreach ($agent in $AgentList) {
|
|
||||||
foreach ($script in $ScriptList) {
|
|
||||||
Build-Variant -Agent $agent -Script $script
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`nArchives in ${GenReleasesDir}:"
|
|
||||||
Get-ChildItem -Path $GenReleasesDir -Filter "spec-kit-template-*-${Version}.zip" | ForEach-Object {
|
|
||||||
Write-Host " $($_.Name)"
|
|
||||||
}
|
|
||||||
389
.github/workflows/scripts/create-release-packages.sh
vendored
389
.github/workflows/scripts/create-release-packages.sh
vendored
@@ -1,389 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# create-release-packages.sh (workflow-local)
|
|
||||||
# Build Spec Kit template release archives for each supported AI assistant and script type.
|
|
||||||
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
|
||||||
# Version argument should include leading 'v'.
|
|
||||||
# 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 junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow generic (default: all)
|
|
||||||
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
|
|
||||||
# Examples:
|
|
||||||
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
|
|
||||||
# AGENTS="copilot,gemini" $0 v0.2.0
|
|
||||||
# SCRIPTS=ps $0 v0.2.0
|
|
||||||
|
|
||||||
if [[ $# -ne 1 ]]; then
|
|
||||||
echo "Usage: $0 <version-with-v-prefix>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
NEW_VERSION="$1"
|
|
||||||
if [[ ! $NEW_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
echo "Version must look like v0.0.0" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building release packages for $NEW_VERSION"
|
|
||||||
|
|
||||||
# Create and use .genreleases directory for all build artifacts
|
|
||||||
# Override via GENRELEASES_DIR env var (e.g. for tests writing to a temp dir)
|
|
||||||
GENRELEASES_DIR="${GENRELEASES_DIR:-.genreleases}"
|
|
||||||
|
|
||||||
# Guard against unsafe GENRELEASES_DIR values before cleaning
|
|
||||||
if [[ -z "$GENRELEASES_DIR" ]]; then
|
|
||||||
echo "GENRELEASES_DIR must not be empty" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
case "$GENRELEASES_DIR" in
|
|
||||||
'/'|'.'|'..')
|
|
||||||
echo "Refusing to use unsafe GENRELEASES_DIR value: $GENRELEASES_DIR" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
if [[ "$GENRELEASES_DIR" == *".."* ]]; then
|
|
||||||
echo "Refusing to use GENRELEASES_DIR containing '..' path segments: $GENRELEASES_DIR" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$GENRELEASES_DIR"
|
|
||||||
rm -rf "${GENRELEASES_DIR%/}/"* || true
|
|
||||||
|
|
||||||
rewrite_paths() {
|
|
||||||
sed -E \
|
|
||||||
-e 's@(/?)memory/@.specify/memory/@g' \
|
|
||||||
-e 's@(/?)scripts/@.specify/scripts/@g' \
|
|
||||||
-e 's@(/?)templates/@.specify/templates/@g' \
|
|
||||||
-e 's@\.specify\.specify/@.specify/@g'
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_commands() {
|
|
||||||
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5
|
|
||||||
mkdir -p "$output_dir"
|
|
||||||
for template in templates/commands/*.md; do
|
|
||||||
[[ -f "$template" ]] || continue
|
|
||||||
local name description script_command agent_script_command body
|
|
||||||
name=$(basename "$template" .md)
|
|
||||||
|
|
||||||
# Normalize line endings
|
|
||||||
file_content=$(tr -d '\r' < "$template")
|
|
||||||
|
|
||||||
# Extract description and script command from YAML frontmatter
|
|
||||||
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
|
||||||
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
|
||||||
|
|
||||||
if [[ -z $script_command ]]; then
|
|
||||||
echo "Warning: no script command found for $script_variant in $template" >&2
|
|
||||||
script_command="(Missing script command for $script_variant)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract agent_script command from YAML frontmatter if present
|
|
||||||
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
|
||||||
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
|
||||||
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
|
|
||||||
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
|
|
||||||
print
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
|
||||||
')
|
|
||||||
|
|
||||||
# Replace {SCRIPT} placeholder with the script command
|
|
||||||
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
|
||||||
|
|
||||||
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
|
||||||
if [[ -n $agent_script_command ]]; then
|
|
||||||
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
|
|
||||||
body=$(printf '%s\n' "$body" | awk '
|
|
||||||
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
|
||||||
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
|
|
||||||
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
|
|
||||||
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
|
|
||||||
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
|
||||||
{ print }
|
|
||||||
')
|
|
||||||
|
|
||||||
# Apply other substitutions
|
|
||||||
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
|
||||||
|
|
||||||
case $ext in
|
|
||||||
toml)
|
|
||||||
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
|
|
||||||
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;;
|
|
||||||
md)
|
|
||||||
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
|
||||||
agent.md)
|
|
||||||
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_copilot_prompts() {
|
|
||||||
local agents_dir=$1 prompts_dir=$2
|
|
||||||
mkdir -p "$prompts_dir"
|
|
||||||
|
|
||||||
# Generate a .prompt.md file for each .agent.md file
|
|
||||||
for agent_file in "$agents_dir"/speckit.*.agent.md; do
|
|
||||||
[[ -f "$agent_file" ]] || continue
|
|
||||||
|
|
||||||
local basename=$(basename "$agent_file" .agent.md)
|
|
||||||
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
|
||||||
|
|
||||||
cat > "$prompt_file" <<EOF
|
|
||||||
---
|
|
||||||
agent: ${basename}
|
|
||||||
---
|
|
||||||
EOF
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create skills in <skills_dir>/<name>/SKILL.md format.
|
|
||||||
# Most agents use hyphenated names (e.g. speckit-plan); Kimi is the
|
|
||||||
# current dotted-name exception (e.g. speckit.plan).
|
|
||||||
#
|
|
||||||
# Technical debt note:
|
|
||||||
# Keep SKILL.md frontmatter aligned with `install_ai_skills()` and extension
|
|
||||||
# overrides (at minimum: name/description/compatibility/metadata.{author,source}).
|
|
||||||
create_skills() {
|
|
||||||
local skills_dir="$1"
|
|
||||||
local script_variant="$2"
|
|
||||||
local agent_name="$3"
|
|
||||||
local separator="${4:-"-"}"
|
|
||||||
|
|
||||||
for template in templates/commands/*.md; do
|
|
||||||
[[ -f "$template" ]] || continue
|
|
||||||
local name
|
|
||||||
name=$(basename "$template" .md)
|
|
||||||
local skill_name="speckit${separator}${name}"
|
|
||||||
local skill_dir="${skills_dir}/${skill_name}"
|
|
||||||
mkdir -p "$skill_dir"
|
|
||||||
|
|
||||||
local file_content
|
|
||||||
file_content=$(tr -d '\r' < "$template")
|
|
||||||
|
|
||||||
# Extract description from frontmatter
|
|
||||||
local description
|
|
||||||
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
|
||||||
[[ -z "$description" ]] && description="Spec Kit: ${name} workflow"
|
|
||||||
|
|
||||||
# Extract script command
|
|
||||||
local script_command
|
|
||||||
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
|
||||||
[[ -z "$script_command" ]] && script_command="(Missing script command for $script_variant)"
|
|
||||||
|
|
||||||
# Extract agent_script command from frontmatter if present
|
|
||||||
local agent_script_command
|
|
||||||
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
|
||||||
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
|
||||||
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
|
|
||||||
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
|
|
||||||
print
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
|
||||||
')
|
|
||||||
|
|
||||||
# Build body: replace placeholders, strip scripts sections, rewrite paths
|
|
||||||
local body
|
|
||||||
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
|
||||||
if [[ -n $agent_script_command ]]; then
|
|
||||||
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
|
||||||
fi
|
|
||||||
body=$(printf '%s\n' "$body" | awk '
|
|
||||||
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
|
||||||
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
|
|
||||||
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
|
|
||||||
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
|
|
||||||
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
|
||||||
{ print }
|
|
||||||
')
|
|
||||||
body=$(printf '%s\n' "$body" | sed 's/{ARGS}/\$ARGUMENTS/g' | sed "s/__AGENT__/$agent_name/g" | rewrite_paths)
|
|
||||||
|
|
||||||
# Strip existing frontmatter and prepend skills frontmatter.
|
|
||||||
local template_body
|
|
||||||
template_body=$(printf '%s\n' "$body" | awk '/^---/{p++; if(p==2){found=1; next}} found')
|
|
||||||
|
|
||||||
{
|
|
||||||
printf -- '---\n'
|
|
||||||
printf 'name: "%s"\n' "$skill_name"
|
|
||||||
printf 'description: "%s"\n' "$description"
|
|
||||||
printf 'compatibility: "%s"\n' "Requires spec-kit project structure with .specify/ directory"
|
|
||||||
printf -- 'metadata:\n'
|
|
||||||
printf ' author: "%s"\n' "github-spec-kit"
|
|
||||||
printf ' source: "%s"\n' "templates/commands/${name}.md"
|
|
||||||
printf -- '---\n\n'
|
|
||||||
printf '%s\n' "$template_body"
|
|
||||||
} > "$skill_dir/SKILL.md"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
build_variant() {
|
|
||||||
local agent=$1 script=$2
|
|
||||||
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
|
||||||
echo "Building $agent ($script) package..."
|
|
||||||
mkdir -p "$base_dir"
|
|
||||||
|
|
||||||
# Copy base structure but filter scripts by variant
|
|
||||||
SPEC_DIR="$base_dir/.specify"
|
|
||||||
mkdir -p "$SPEC_DIR"
|
|
||||||
|
|
||||||
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
|
|
||||||
|
|
||||||
# Only copy the relevant script variant directory
|
|
||||||
if [[ -d scripts ]]; then
|
|
||||||
mkdir -p "$SPEC_DIR/scripts"
|
|
||||||
case $script in
|
|
||||||
sh)
|
|
||||||
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
|
|
||||||
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
|
||||||
;;
|
|
||||||
ps)
|
|
||||||
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
|
|
||||||
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" | while IFS= read -r f; do d="$SPEC_DIR/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done; echo "Copied templates -> .specify/templates"; }
|
|
||||||
|
|
||||||
case $agent in
|
|
||||||
claude)
|
|
||||||
mkdir -p "$base_dir/.claude/commands"
|
|
||||||
generate_commands claude md "\$ARGUMENTS" "$base_dir/.claude/commands" "$script" ;;
|
|
||||||
gemini)
|
|
||||||
mkdir -p "$base_dir/.gemini/commands"
|
|
||||||
generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script"
|
|
||||||
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;;
|
|
||||||
copilot)
|
|
||||||
mkdir -p "$base_dir/.github/agents"
|
|
||||||
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
|
|
||||||
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
|
||||||
mkdir -p "$base_dir/.vscode"
|
|
||||||
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
|
|
||||||
;;
|
|
||||||
cursor-agent)
|
|
||||||
mkdir -p "$base_dir/.cursor/commands"
|
|
||||||
generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;;
|
|
||||||
qwen)
|
|
||||||
mkdir -p "$base_dir/.qwen/commands"
|
|
||||||
generate_commands qwen md "\$ARGUMENTS" "$base_dir/.qwen/commands" "$script"
|
|
||||||
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
|
|
||||||
opencode)
|
|
||||||
mkdir -p "$base_dir/.opencode/command"
|
|
||||||
generate_commands opencode md "\$ARGUMENTS" "$base_dir/.opencode/command" "$script" ;;
|
|
||||||
windsurf)
|
|
||||||
mkdir -p "$base_dir/.windsurf/workflows"
|
|
||||||
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
|
|
||||||
junie)
|
|
||||||
mkdir -p "$base_dir/.junie/commands"
|
|
||||||
generate_commands junie md "\$ARGUMENTS" "$base_dir/.junie/commands" "$script" ;;
|
|
||||||
codex)
|
|
||||||
mkdir -p "$base_dir/.agents/skills"
|
|
||||||
create_skills "$base_dir/.agents/skills" "$script" "codex" "-" ;;
|
|
||||||
kilocode)
|
|
||||||
mkdir -p "$base_dir/.kilocode/workflows"
|
|
||||||
generate_commands kilocode md "\$ARGUMENTS" "$base_dir/.kilocode/workflows" "$script" ;;
|
|
||||||
auggie)
|
|
||||||
mkdir -p "$base_dir/.augment/commands"
|
|
||||||
generate_commands auggie md "\$ARGUMENTS" "$base_dir/.augment/commands" "$script" ;;
|
|
||||||
roo)
|
|
||||||
mkdir -p "$base_dir/.roo/commands"
|
|
||||||
generate_commands roo md "\$ARGUMENTS" "$base_dir/.roo/commands" "$script" ;;
|
|
||||||
codebuddy)
|
|
||||||
mkdir -p "$base_dir/.codebuddy/commands"
|
|
||||||
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
|
|
||||||
qodercli)
|
|
||||||
mkdir -p "$base_dir/.qoder/commands"
|
|
||||||
generate_commands qodercli md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
|
|
||||||
amp)
|
|
||||||
mkdir -p "$base_dir/.agents/commands"
|
|
||||||
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
|
|
||||||
shai)
|
|
||||||
mkdir -p "$base_dir/.shai/commands"
|
|
||||||
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
|
|
||||||
tabnine)
|
|
||||||
mkdir -p "$base_dir/.tabnine/agent/commands"
|
|
||||||
generate_commands tabnine toml "{{args}}" "$base_dir/.tabnine/agent/commands" "$script"
|
|
||||||
[[ -f agent_templates/tabnine/TABNINE.md ]] && cp agent_templates/tabnine/TABNINE.md "$base_dir/TABNINE.md" ;;
|
|
||||||
kiro-cli)
|
|
||||||
mkdir -p "$base_dir/.kiro/prompts"
|
|
||||||
generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;;
|
|
||||||
agy)
|
|
||||||
mkdir -p "$base_dir/.agent/commands"
|
|
||||||
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/commands" "$script" ;;
|
|
||||||
bob)
|
|
||||||
mkdir -p "$base_dir/.bob/commands"
|
|
||||||
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
|
||||||
vibe)
|
|
||||||
mkdir -p "$base_dir/.vibe/prompts"
|
|
||||||
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
|
|
||||||
kimi)
|
|
||||||
mkdir -p "$base_dir/.kimi/skills"
|
|
||||||
create_skills "$base_dir/.kimi/skills" "$script" "kimi" "." ;;
|
|
||||||
trae)
|
|
||||||
mkdir -p "$base_dir/.trae/rules"
|
|
||||||
generate_commands trae md "\$ARGUMENTS" "$base_dir/.trae/rules" "$script" ;;
|
|
||||||
pi)
|
|
||||||
mkdir -p "$base_dir/.pi/prompts"
|
|
||||||
generate_commands pi md "\$ARGUMENTS" "$base_dir/.pi/prompts" "$script" ;;
|
|
||||||
iflow)
|
|
||||||
mkdir -p "$base_dir/.iflow/commands"
|
|
||||||
generate_commands iflow md "\$ARGUMENTS" "$base_dir/.iflow/commands" "$script" ;;
|
|
||||||
generic)
|
|
||||||
mkdir -p "$base_dir/.speckit/commands"
|
|
||||||
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
|
||||||
esac
|
|
||||||
( 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"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine agent list
|
|
||||||
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf junie codex kilocode auggie roo codebuddy amp shai tabnine kiro-cli agy bob vibe qodercli kimi trae pi iflow generic)
|
|
||||||
ALL_SCRIPTS=(sh ps)
|
|
||||||
|
|
||||||
validate_subset() {
|
|
||||||
local type=$1; shift
|
|
||||||
local allowed_str="$1"; shift
|
|
||||||
local invalid=0
|
|
||||||
for it in "$@"; do
|
|
||||||
local found=0
|
|
||||||
for a in $allowed_str; do
|
|
||||||
if [[ "$it" == "$a" ]]; then found=1; break; fi
|
|
||||||
done
|
|
||||||
if [[ $found -eq 0 ]]; then
|
|
||||||
echo "Error: unknown $type '$it' (allowed: $allowed_str)" >&2
|
|
||||||
invalid=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return $invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
read_list() { tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i);out=1}}}END{printf("\n")}'; }
|
|
||||||
|
|
||||||
if [[ -n ${AGENTS:-} ]]; then
|
|
||||||
read -ra AGENT_LIST <<< "$(printf '%s' "$AGENTS" | read_list)"
|
|
||||||
validate_subset agent "${ALL_AGENTS[*]}" "${AGENT_LIST[@]}" || exit 1
|
|
||||||
else
|
|
||||||
AGENT_LIST=("${ALL_AGENTS[@]}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n ${SCRIPTS:-} ]]; then
|
|
||||||
read -ra SCRIPT_LIST <<< "$(printf '%s' "$SCRIPTS" | read_list)"
|
|
||||||
validate_subset script "${ALL_SCRIPTS[*]}" "${SCRIPT_LIST[@]}" || exit 1
|
|
||||||
else
|
|
||||||
SCRIPT_LIST=("${ALL_SCRIPTS[@]}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Agents: ${AGENT_LIST[*]}"
|
|
||||||
echo "Scripts: ${SCRIPT_LIST[*]}"
|
|
||||||
|
|
||||||
for agent in "${AGENT_LIST[@]}"; do
|
|
||||||
for script in "${SCRIPT_LIST[@]}"; do
|
|
||||||
build_variant "$agent" "$script"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Archives in $GENRELEASES_DIR:"
|
|
||||||
ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# generate-release-notes.sh
|
|
||||||
# Generate release notes from git history
|
|
||||||
# Usage: generate-release-notes.sh <new_version> <last_tag>
|
|
||||||
|
|
||||||
if [[ $# -ne 2 ]]; then
|
|
||||||
echo "Usage: $0 <new_version> <last_tag>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
NEW_VERSION="$1"
|
|
||||||
LAST_TAG="$2"
|
|
||||||
|
|
||||||
# Get commits since last tag
|
|
||||||
if [ "$LAST_TAG" = "v0.0.0" ]; then
|
|
||||||
# Check how many commits we have and use that as the limit
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
if [ "$COMMIT_COUNT" -gt 10 ]; then
|
|
||||||
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD)
|
|
||||||
else
|
|
||||||
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s")
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create release notes
|
|
||||||
cat > release_notes.md << EOF
|
|
||||||
This is the latest set of releases that you can use with your agent of choice. We recommend using the Specify CLI to scaffold your projects, however you can download these independently and manage them yourself.
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
$COMMITS
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Generated release notes:"
|
|
||||||
cat release_notes.md
|
|
||||||
24
.github/workflows/scripts/get-next-version.sh
vendored
24
.github/workflows/scripts/get-next-version.sh
vendored
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# get-next-version.sh
|
|
||||||
# Calculate the next version based on the latest git tag and output GitHub Actions variables
|
|
||||||
# Usage: get-next-version.sh
|
|
||||||
|
|
||||||
# Get the latest tag, or use v0.0.0 if no tags exist
|
|
||||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
||||||
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Extract version number and increment
|
|
||||||
VERSION=$(echo $LATEST_TAG | sed 's/v//')
|
|
||||||
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
|
|
||||||
MAJOR=${VERSION_PARTS[0]:-0}
|
|
||||||
MINOR=${VERSION_PARTS[1]:-0}
|
|
||||||
PATCH=${VERSION_PARTS[2]:-0}
|
|
||||||
|
|
||||||
# Increment patch version
|
|
||||||
PATCH=$((PATCH + 1))
|
|
||||||
NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
|
|
||||||
|
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "New version will be: $NEW_VERSION"
|
|
||||||
161
.github/workflows/scripts/simulate-release.sh
vendored
161
.github/workflows/scripts/simulate-release.sh
vendored
@@ -1,161 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# simulate-release.sh
|
|
||||||
# Simulate the release process locally without pushing to GitHub
|
|
||||||
# Usage: simulate-release.sh [version]
|
|
||||||
# If version is omitted, auto-increments patch version
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${BLUE}🧪 Simulating Release Process Locally${NC}"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Step 1: Determine version
|
|
||||||
if [[ -n "${1:-}" ]]; then
|
|
||||||
VERSION="${1#v}"
|
|
||||||
TAG="v$VERSION"
|
|
||||||
echo -e "${GREEN}📝 Using manual version: $VERSION${NC}"
|
|
||||||
else
|
|
||||||
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
|
||||||
echo -e "${BLUE}Latest tag: $LATEST_TAG${NC}"
|
|
||||||
|
|
||||||
VERSION=$(echo $LATEST_TAG | sed 's/v//')
|
|
||||||
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
|
|
||||||
MAJOR=${VERSION_PARTS[0]:-0}
|
|
||||||
MINOR=${VERSION_PARTS[1]:-0}
|
|
||||||
PATCH=${VERSION_PARTS[2]:-0}
|
|
||||||
|
|
||||||
PATCH=$((PATCH + 1))
|
|
||||||
VERSION="$MAJOR.$MINOR.$PATCH"
|
|
||||||
TAG="v$VERSION"
|
|
||||||
echo -e "${GREEN}📝 Auto-incremented to: $VERSION${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Step 2: Check if tag exists
|
|
||||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
||||||
echo -e "${RED}❌ Error: Tag $TAG already exists!${NC}"
|
|
||||||
echo " Please use a different version or delete the tag first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}✓ Tag $TAG is available${NC}"
|
|
||||||
|
|
||||||
# Step 3: Backup current state
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}💾 Creating backup of current state...${NC}"
|
|
||||||
BACKUP_DIR=$(mktemp -d)
|
|
||||||
cp pyproject.toml "$BACKUP_DIR/pyproject.toml.bak"
|
|
||||||
cp CHANGELOG.md "$BACKUP_DIR/CHANGELOG.md.bak"
|
|
||||||
echo -e "${GREEN}✓ Backup created at: $BACKUP_DIR${NC}"
|
|
||||||
|
|
||||||
# Step 4: Update pyproject.toml
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}📝 Updating pyproject.toml...${NC}"
|
|
||||||
sed -i.tmp "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml
|
|
||||||
rm -f pyproject.toml.tmp
|
|
||||||
echo -e "${GREEN}✓ Updated pyproject.toml to version $VERSION${NC}"
|
|
||||||
|
|
||||||
# Step 5: Update CHANGELOG.md
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}📝 Updating CHANGELOG.md...${NC}"
|
|
||||||
DATE=$(date +%Y-%m-%d)
|
|
||||||
|
|
||||||
# Get the previous tag to compare commits
|
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [[ -n "$PREVIOUS_TAG" ]]; then
|
|
||||||
echo " Generating changelog from commits since $PREVIOUS_TAG"
|
|
||||||
# Get commits since last tag, format as bullet points
|
|
||||||
COMMITS=$(git log --oneline "$PREVIOUS_TAG"..HEAD --no-merges --pretty=format:"- %s" 2>/dev/null || echo "- Initial release")
|
|
||||||
else
|
|
||||||
echo " No previous tag found - this is the first release"
|
|
||||||
COMMITS="- Initial release"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create temp file with new entry
|
|
||||||
{
|
|
||||||
head -n 8 CHANGELOG.md
|
|
||||||
echo ""
|
|
||||||
echo "## [$VERSION] - $DATE"
|
|
||||||
echo ""
|
|
||||||
echo "### Changed"
|
|
||||||
echo ""
|
|
||||||
echo "$COMMITS"
|
|
||||||
echo ""
|
|
||||||
tail -n +9 CHANGELOG.md
|
|
||||||
} > CHANGELOG.md.tmp
|
|
||||||
mv CHANGELOG.md.tmp CHANGELOG.md
|
|
||||||
echo -e "${GREEN}✓ Updated CHANGELOG.md with commits since $PREVIOUS_TAG${NC}"
|
|
||||||
|
|
||||||
# Step 6: Show what would be committed
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}📋 Changes that would be committed:${NC}"
|
|
||||||
git diff pyproject.toml CHANGELOG.md
|
|
||||||
|
|
||||||
# Step 7: Create temporary tag (no push)
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}🏷️ Creating temporary local tag...${NC}"
|
|
||||||
git tag -a "$TAG" -m "Simulated release $TAG" 2>/dev/null || true
|
|
||||||
echo -e "${GREEN}✓ Tag $TAG created locally${NC}"
|
|
||||||
|
|
||||||
# Step 8: Simulate release artifact creation
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}📦 Simulating release package creation...${NC}"
|
|
||||||
echo " (High-level simulation only; packaging script is not executed)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if script exists and is executable
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
if [[ -x "$SCRIPT_DIR/create-release-packages.sh" ]]; then
|
|
||||||
echo -e "${BLUE}In a real release, the following command would be run to create packages:${NC}"
|
|
||||||
echo " $SCRIPT_DIR/create-release-packages.sh \"$TAG\""
|
|
||||||
echo ""
|
|
||||||
echo "This simulation does not enumerate individual package files to avoid"
|
|
||||||
echo "drifting from the actual behavior of create-release-packages.sh."
|
|
||||||
else
|
|
||||||
echo -e "${RED}⚠️ create-release-packages.sh not found or not executable${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 9: Simulate release notes generation
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}📄 Simulating release notes generation...${NC}"
|
|
||||||
echo ""
|
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 $TAG^ 2>/dev/null || echo "")
|
|
||||||
if [[ -n "$PREVIOUS_TAG" ]]; then
|
|
||||||
echo -e "${BLUE}Changes since $PREVIOUS_TAG:${NC}"
|
|
||||||
git log --oneline "$PREVIOUS_TAG".."$TAG" | head -n 10
|
|
||||||
echo ""
|
|
||||||
else
|
|
||||||
echo -e "${BLUE}No previous tag found - this would be the first release${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 10: Summary
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}🎉 Simulation Complete!${NC}"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}Summary:${NC}"
|
|
||||||
echo " Version: $VERSION"
|
|
||||||
echo " Tag: $TAG"
|
|
||||||
echo " Backup: $BACKUP_DIR"
|
|
||||||
echo ""
|
|
||||||
echo -e "${YELLOW}⚠️ SIMULATION ONLY - NO CHANGES PUSHED${NC}"
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}Next steps:${NC}"
|
|
||||||
echo " 1. Review the changes above"
|
|
||||||
echo " 2. To keep changes: git add pyproject.toml CHANGELOG.md && git commit"
|
|
||||||
echo " 3. To discard changes: git checkout pyproject.toml CHANGELOG.md && git tag -d $TAG"
|
|
||||||
echo " 4. To restore from backup: cp $BACKUP_DIR/* ."
|
|
||||||
echo ""
|
|
||||||
echo -e "${BLUE}To run the actual release:${NC}"
|
|
||||||
echo " Go to: https://github.com/github/spec-kit/actions/workflows/release-trigger.yml"
|
|
||||||
echo " Click 'Run workflow' and enter version: $VERSION"
|
|
||||||
echo ""
|
|
||||||
23
.github/workflows/scripts/update-version.sh
vendored
23
.github/workflows/scripts/update-version.sh
vendored
@@ -1,23 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# update-version.sh
|
|
||||||
# Update version in pyproject.toml (for release artifacts only)
|
|
||||||
# Usage: update-version.sh <version>
|
|
||||||
|
|
||||||
if [[ $# -ne 1 ]]; then
|
|
||||||
echo "Usage: $0 <version>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION="$1"
|
|
||||||
|
|
||||||
# Remove 'v' prefix for Python versioning
|
|
||||||
PYTHON_VERSION=${VERSION#v}
|
|
||||||
|
|
||||||
if [ -f "pyproject.toml" ]; then
|
|
||||||
sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml
|
|
||||||
echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)"
|
|
||||||
else
|
|
||||||
echo "Warning: pyproject.toml not found, skipping version update"
|
|
||||||
fi
|
|
||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
|
|||||||
36
AGENTS.md
36
AGENTS.md
@@ -30,10 +30,10 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
|
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
|
||||||
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI |
|
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI |
|
||||||
| **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
|
| **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
|
||||||
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
|
| **Cursor** | `.cursor/commands/` | Markdown | N/A (IDE-based) | Cursor IDE (`--ai cursor-agent`) |
|
||||||
| **Qwen Code** | `.qwen/commands/` | Markdown | `qwen` | Alibaba's Qwen Code CLI |
|
| **Qwen Code** | `.qwen/commands/` | Markdown | `qwen` | Alibaba's Qwen Code CLI |
|
||||||
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
||||||
| **Codex CLI** | `.agents/skills/` | Markdown | `codex` | Codex CLI (skills) |
|
| **Codex CLI** | `.agents/skills/` | Markdown | `codex` | Codex CLI (`--ai codex --ai-skills`) |
|
||||||
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
|
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
|
||||||
| **Junie** | `.junie/commands/` | Markdown | `junie` | Junie by JetBrains |
|
| **Junie** | `.junie/commands/` | Markdown | `junie` | Junie by JetBrains |
|
||||||
| **Kilo Code** | `.kilocode/workflows/` | Markdown | N/A (IDE-based) | Kilo Code IDE |
|
| **Kilo Code** | `.kilocode/workflows/` | Markdown | N/A (IDE-based) | Kilo Code IDE |
|
||||||
@@ -50,6 +50,8 @@ Specify supports multiple AI agents by generating agent-specific command files a
|
|||||||
| **iFlow CLI** | `.iflow/commands/` | Markdown | `iflow` | iFlow CLI (iflow-ai) |
|
| **iFlow CLI** | `.iflow/commands/` | Markdown | `iflow` | iFlow CLI (iflow-ai) |
|
||||||
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
||||||
| **Trae** | `.trae/rules/` | Markdown | N/A (IDE-based) | Trae IDE |
|
| **Trae** | `.trae/rules/` | Markdown | N/A (IDE-based) | Trae IDE |
|
||||||
|
| **Antigravity** | `.agent/commands/` | Markdown | N/A (IDE-based) | Antigravity IDE (`--ai agy --ai-skills`) |
|
||||||
|
| **Mistral Vibe** | `.vibe/prompts/` | Markdown | `vibe` | Mistral Vibe CLI |
|
||||||
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
|
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
|
||||||
|
|
||||||
### Step-by-Step Integration Guide
|
### Step-by-Step Integration Guide
|
||||||
@@ -316,32 +318,40 @@ Require a command-line tool to be installed:
|
|||||||
|
|
||||||
- **Claude Code**: `claude` CLI
|
- **Claude Code**: `claude` CLI
|
||||||
- **Gemini CLI**: `gemini` CLI
|
- **Gemini CLI**: `gemini` CLI
|
||||||
- **Cursor**: `cursor-agent` CLI
|
|
||||||
- **Qwen Code**: `qwen` CLI
|
- **Qwen Code**: `qwen` CLI
|
||||||
- **opencode**: `opencode` CLI
|
- **opencode**: `opencode` CLI
|
||||||
|
- **Codex CLI**: `codex` CLI (requires `--ai-skills`)
|
||||||
- **Junie**: `junie` CLI
|
- **Junie**: `junie` CLI
|
||||||
- **Kiro CLI**: `kiro-cli` CLI
|
- **Auggie CLI**: `auggie` CLI
|
||||||
- **CodeBuddy CLI**: `codebuddy` CLI
|
- **CodeBuddy CLI**: `codebuddy` CLI
|
||||||
- **Qoder CLI**: `qodercli` CLI
|
- **Qoder CLI**: `qodercli` CLI
|
||||||
|
- **Kiro CLI**: `kiro-cli` CLI
|
||||||
- **Amp**: `amp` CLI
|
- **Amp**: `amp` CLI
|
||||||
- **SHAI**: `shai` CLI
|
- **SHAI**: `shai` CLI
|
||||||
- **Tabnine CLI**: `tabnine` CLI
|
- **Tabnine CLI**: `tabnine` CLI
|
||||||
- **Kimi Code**: `kimi` CLI
|
- **Kimi Code**: `kimi` CLI
|
||||||
|
- **Mistral Vibe**: `vibe` CLI
|
||||||
- **Pi Coding Agent**: `pi` CLI
|
- **Pi Coding Agent**: `pi` CLI
|
||||||
|
- **iFlow CLI**: `iflow` CLI
|
||||||
|
|
||||||
### IDE-Based Agents
|
### IDE-Based Agents
|
||||||
|
|
||||||
Work within integrated development environments:
|
Work within integrated development environments:
|
||||||
|
|
||||||
- **GitHub Copilot**: Built into VS Code/compatible editors
|
- **GitHub Copilot**: Built into VS Code/compatible editors
|
||||||
|
- **Cursor**: Built into Cursor IDE (`--ai cursor-agent`)
|
||||||
- **Windsurf**: Built into Windsurf IDE
|
- **Windsurf**: Built into Windsurf IDE
|
||||||
|
- **Kilo Code**: Built into Kilo Code IDE
|
||||||
|
- **Roo Code**: Built into Roo Code IDE
|
||||||
- **IBM Bob**: Built into IBM Bob IDE
|
- **IBM Bob**: Built into IBM Bob IDE
|
||||||
|
- **Trae**: Built into Trae IDE
|
||||||
|
- **Antigravity**: Built into Antigravity IDE (`--ai agy --ai-skills`)
|
||||||
|
|
||||||
## Command File Formats
|
## Command File Formats
|
||||||
|
|
||||||
### Markdown Format
|
### Markdown Format
|
||||||
|
|
||||||
Used by: Claude, Cursor, opencode, Windsurf, Junie, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen, Pi
|
Used by: Claude, Cursor, GitHub Copilot, opencode, Windsurf, Junie, Kiro CLI, Amp, SHAI, IBM Bob, Kimi Code, Qwen, Pi, Codex, Auggie, CodeBuddy, Qoder, Roo Code, Kilo Code, Trae, Antigravity, Mistral Vibe, iFlow
|
||||||
|
|
||||||
**Standard format:**
|
**Standard format:**
|
||||||
|
|
||||||
@@ -379,15 +389,29 @@ Command content with {SCRIPT} and {{args}} placeholders.
|
|||||||
## Directory Conventions
|
## Directory Conventions
|
||||||
|
|
||||||
- **CLI agents**: Usually `.<agent-name>/commands/`
|
- **CLI agents**: Usually `.<agent-name>/commands/`
|
||||||
|
- **Singular command exception**:
|
||||||
|
- opencode: `.opencode/command/` (singular `command`, not `commands`)
|
||||||
|
- **Nested path exception**:
|
||||||
|
- Tabnine: `.tabnine/agent/commands/` (extra `agent/` segment)
|
||||||
|
- **Shared `.agents/` folder**:
|
||||||
|
- Amp: `.agents/commands/` (shared folder, not `.amp/`)
|
||||||
|
- Codex: `.agents/skills/` (shared folder; requires `--ai-skills`; invoked as `$speckit-<command>`)
|
||||||
- **Skills-based exceptions**:
|
- **Skills-based exceptions**:
|
||||||
- Codex: `.agents/skills/` (skills, invoked as `$speckit-<command>`)
|
- Kimi Code: `.kimi/skills/` (skills, invoked as `/skill:speckit-<command>`)
|
||||||
- **Prompt-based exceptions**:
|
- **Prompt-based exceptions**:
|
||||||
- Kiro CLI: `.kiro/prompts/`
|
- Kiro CLI: `.kiro/prompts/`
|
||||||
- Pi: `.pi/prompts/`
|
- Pi: `.pi/prompts/`
|
||||||
|
- Mistral Vibe: `.vibe/prompts/`
|
||||||
|
- **Rules-based exceptions**:
|
||||||
|
- Trae: `.trae/rules/`
|
||||||
- **IDE agents**: Follow IDE-specific patterns:
|
- **IDE agents**: Follow IDE-specific patterns:
|
||||||
- Copilot: `.github/agents/`
|
- Copilot: `.github/agents/`
|
||||||
- Cursor: `.cursor/commands/`
|
- Cursor: `.cursor/commands/`
|
||||||
- Windsurf: `.windsurf/workflows/`
|
- Windsurf: `.windsurf/workflows/`
|
||||||
|
- Kilo Code: `.kilocode/workflows/`
|
||||||
|
- Roo Code: `.roo/commands/`
|
||||||
|
- IBM Bob: `.bob/commands/`
|
||||||
|
- Antigravity: `.agent/skills/` (`--ai-skills` required; `.agent/commands/` is deprecated)
|
||||||
|
|
||||||
## Argument Patterns
|
## Argument Patterns
|
||||||
|
|
||||||
|
|||||||
1114
CHANGELOG.md
1114
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler
|
|||||||
> If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed.
|
> If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed.
|
||||||
|
|
||||||
1. Fork and clone the repository
|
1. Fork and clone the repository
|
||||||
1. Configure and install the dependencies: `uv sync`
|
1. Configure and install the dependencies: `uv sync --extra test`
|
||||||
1. Make sure the CLI works on your machine: `uv run specify --help`
|
1. Make sure the CLI works on your machine: `uv run specify --help`
|
||||||
1. Create a new branch: `git checkout -b my-branch-name`
|
1. Create a new branch: `git checkout -b my-branch-name`
|
||||||
1. Make your change, add tests, and make sure everything still works
|
1. Make your change, add tests, and make sure everything still works
|
||||||
@@ -44,6 +44,9 @@ On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler
|
|||||||
1. Push to your fork and submit a pull request
|
1. Push to your fork and submit a pull request
|
||||||
1. Wait for your pull request to be reviewed and merged.
|
1. Wait for your pull request to be reviewed and merged.
|
||||||
|
|
||||||
|
For the detailed test workflow, command-selection prompt, and PR reporting template, see [`TESTING.md`](./TESTING.md).
|
||||||
|
Activate the project virtual environment (see the Setup block in [`TESTING.md`](./TESTING.md)), then install the CLI from your working tree (`uv pip install -e .` after `uv sync --extra test`) or otherwise ensure the shell uses the local `specify` binary before running the manual slash-command tests described below.
|
||||||
|
|
||||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||||
|
|
||||||
- Follow the project's coding conventions.
|
- Follow the project's coding conventions.
|
||||||
@@ -62,6 +65,14 @@ When working on spec-kit:
|
|||||||
3. Test script functionality in the `scripts/` directory
|
3. Test script functionality in the `scripts/` directory
|
||||||
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
|
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
|
||||||
|
|
||||||
|
### Recommended validation flow
|
||||||
|
|
||||||
|
For the smoothest review experience, validate changes in this order:
|
||||||
|
|
||||||
|
1. **Run focused automated checks first** — use the quick verification commands in [`TESTING.md`](./TESTING.md) to catch packaging, scaffolding, and configuration regressions early.
|
||||||
|
2. **Run manual workflow tests second** — if your change affects slash commands or the developer workflow, follow [`TESTING.md`](./TESTING.md) to choose the right commands, run them in an agent, and capture results for your PR.
|
||||||
|
3. **Use local release packages when debugging packaged output** — if you need to inspect the exact files CI-style packaging produces, generate local release packages as described below.
|
||||||
|
|
||||||
### Testing template and command changes locally
|
### Testing template and command changes locally
|
||||||
|
|
||||||
Running `uv run specify init` pulls released packages, which won’t include your local changes.
|
Running `uv run specify init` pulls released packages, which won’t include your local changes.
|
||||||
@@ -85,6 +96,8 @@ To test your templates, commands, and other changes locally, follow these steps:
|
|||||||
|
|
||||||
Navigate to your test project folder and open the agent to verify your implementation.
|
Navigate to your test project folder and open the agent to verify your implementation.
|
||||||
|
|
||||||
|
If you only need to validate generated file structure and content before doing manual agent testing, start with the focused automated checks in [`TESTING.md`](./TESTING.md). Keep this section for the cases where you need to inspect the exact packaged output locally.
|
||||||
|
|
||||||
## AI contributions in Spec Kit
|
## AI contributions in Spec Kit
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
63
README.md
63
README.md
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/github/spec-kit/actions/workflows/release.yml"><img src="https://github.com/github/spec-kit/actions/workflows/release.yml/badge.svg" alt="Release"/></a>
|
<a href="https://github.com/github/spec-kit/releases/latest"><img src="https://img.shields.io/github/v/release/github/spec-kit" alt="Latest Release"/></a>
|
||||||
<a href="https://github.com/github/spec-kit/stargazers"><img src="https://img.shields.io/github/stars/github/spec-kit?style=social" alt="GitHub stars"/></a>
|
<a href="https://github.com/github/spec-kit/stargazers"><img src="https://img.shields.io/github/stars/github/spec-kit?style=social" alt="GitHub stars"/></a>
|
||||||
<a href="https://github.com/github/spec-kit/blob/main/LICENSE"><img src="https://img.shields.io/github/license/github/spec-kit" alt="License"/></a>
|
<a href="https://github.com/github/spec-kit/blob/main/LICENSE"><img src="https://img.shields.io/github/license/github/spec-kit" alt="License"/></a>
|
||||||
<a href="https://github.github.io/spec-kit/"><img src="https://img.shields.io/badge/docs-GitHub_Pages-blue" alt="Documentation"/></a>
|
<a href="https://github.github.io/spec-kit/"><img src="https://img.shields.io/badge/docs-GitHub_Pages-blue" alt="Documentation"/></a>
|
||||||
@@ -160,11 +160,25 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
|
|||||||
|
|
||||||
## 🧩 Community Extensions
|
## 🧩 Community Extensions
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
|
||||||
|
|
||||||
|
🔍 **Browse and search community extensions on the [Community Extensions website](https://speckit-community.github.io/extensions/).**
|
||||||
|
|
||||||
The following community-contributed extensions are available in [`catalog.community.json`](extensions/catalog.community.json):
|
The following community-contributed extensions are available in [`catalog.community.json`](extensions/catalog.community.json):
|
||||||
|
|
||||||
**Categories:** `docs` — reads, validates, or generates spec artifacts · `code` — reviews, validates, or modifies source code · `process` — orchestrates workflow across phases · `integration` — syncs with external platforms · `visibility` — reports on project health or progress
|
**Categories:**
|
||||||
|
|
||||||
**Effect:** `Read-only` — produces reports without modifying files · `Read+Write` — modifies files, creates artifacts, or updates specs
|
- `docs` — reads, validates, or generates spec artifacts
|
||||||
|
- `code` — reviews, validates, or modifies source code
|
||||||
|
- `process` — orchestrates workflow across phases
|
||||||
|
- `integration` — syncs with external platforms
|
||||||
|
- `visibility` — reports on project health or progress
|
||||||
|
|
||||||
|
**Effect:**
|
||||||
|
|
||||||
|
- `Read-only` — produces reports without modifying files
|
||||||
|
- `Read+Write` — modifies files, creates artifacts, or updates specs
|
||||||
|
|
||||||
| Extension | Purpose | Category | Effect | URL |
|
| Extension | Purpose | Category | Effect | URL |
|
||||||
|-----------|---------|----------|--------|-----|
|
|-----------|---------|----------|--------|-----|
|
||||||
@@ -173,24 +187,40 @@ The following community-contributed extensions are available in [`catalog.commun
|
|||||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
||||||
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
|
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
|
||||||
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
||||||
| Cognitive Squad | Multi-agent cognitive system with Triadic Model: understanding, internalization, application — with quality gates, backpropagation verification, and self-healing | `docs` | Read+Write | [cognitive-squad](https://github.com/Testimonial/cognitive-squad) |
|
|
||||||
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
||||||
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
||||||
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
|
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
|
||||||
|
| Fix Findings | Automated analyze-fix-reanalyze loop that resolves spec findings until clean | `code` | Read+Write | [spec-kit-fix-findings](https://github.com/Quratulain-bilal/spec-kit-fix-findings) |
|
||||||
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
||||||
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
|
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
|
||||||
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
||||||
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | `docs` | Read+Write | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |
|
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | `docs` | Read+Write | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |
|
||||||
|
| MAQA — Multi-Agent & Quality Assurance | Coordinator → feature → QA agent workflow with parallel worktree-based implementation. Language-agnostic. Auto-detects installed board plugins. Optional CI gate. | `process` | Read+Write | [spec-kit-maqa-ext](https://github.com/GenieRobot/spec-kit-maqa-ext) |
|
||||||
|
| MAQA Azure DevOps Integration | Azure DevOps Boards integration for MAQA — syncs User Stories and Task children as features progress | `integration` | Read+Write | [spec-kit-maqa-azure-devops](https://github.com/GenieRobot/spec-kit-maqa-azure-devops) |
|
||||||
|
| MAQA CI/CD Gate | Auto-detects GitHub Actions, CircleCI, GitLab CI, and Bitbucket Pipelines. Blocks QA handoff until pipeline is green. | `process` | Read+Write | [spec-kit-maqa-ci](https://github.com/GenieRobot/spec-kit-maqa-ci) |
|
||||||
|
| MAQA GitHub Projects Integration | GitHub Projects v2 integration for MAQA — syncs draft issues and Status columns as features progress | `integration` | Read+Write | [spec-kit-maqa-github-projects](https://github.com/GenieRobot/spec-kit-maqa-github-projects) |
|
||||||
|
| MAQA Jira Integration | Jira integration for MAQA — syncs Stories and Subtasks as features progress through the board | `integration` | Read+Write | [spec-kit-maqa-jira](https://github.com/GenieRobot/spec-kit-maqa-jira) |
|
||||||
|
| MAQA Linear Integration | Linear integration for MAQA — syncs issues and sub-issues across workflow states as features progress | `integration` | Read+Write | [spec-kit-maqa-linear](https://github.com/GenieRobot/spec-kit-maqa-linear) |
|
||||||
|
| MAQA Trello Integration | Trello board integration for MAQA — populates board from specs, moves cards, real-time checklist ticking | `integration` | Read+Write | [spec-kit-maqa-trello](https://github.com/GenieRobot/spec-kit-maqa-trello) |
|
||||||
|
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
|
||||||
|
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
|
||||||
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
|
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
|
||||||
|
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
|
||||||
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
||||||
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
|
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
|
||||||
|
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
|
||||||
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
|
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
|
||||||
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
|
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
|
||||||
|
| Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) |
|
||||||
|
| Retro Extension | Sprint retrospective analysis with metrics, spec accuracy assessment, and improvement suggestions | `process` | Read+Write | [spec-kit-retro](https://github.com/arunt14/spec-kit-retro) |
|
||||||
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | `docs` | Read+Write | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
|
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | `docs` | Read+Write | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
|
||||||
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | `code` | Read-only | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
|
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | `code` | Read-only | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
|
||||||
| SDD Utilities | Resume interrupted workflows, validate project health, and verify spec-to-task traceability | `process` | Read+Write | [speckit-utils](https://github.com/mvanhorn/speckit-utils) |
|
| SDD Utilities | Resume interrupted workflows, validate project health, and verify spec-to-task traceability | `process` | Read+Write | [speckit-utils](https://github.com/mvanhorn/speckit-utils) |
|
||||||
|
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
|
||||||
|
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
|
||||||
|
| Ship Release Extension | Automates release pipeline: pre-flight checks, branch sync, changelog generation, CI verification, and PR creation | `process` | Read+Write | [spec-kit-ship](https://github.com/arunt14/spec-kit-ship) |
|
||||||
|
| Spec Critique Extension | Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives | `docs` | Read-only | [spec-kit-critique](https://github.com/arunt14/spec-kit-critique) |
|
||||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||||
| Understanding | Automated requirements quality analysis — 31 deterministic metrics against IEEE/ISO standards with experimental energy-based ambiguity detection | `docs` | Read-only | [understanding](https://github.com/Testimonial/understanding) |
|
|
||||||
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||||
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
||||||
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
||||||
@@ -199,6 +229,9 @@ To submit your own extension, see the [Extension Publishing Guide](extensions/EX
|
|||||||
|
|
||||||
## 🎨 Community Presets
|
## 🎨 Community Presets
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
|
||||||
|
|
||||||
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](presets/catalog.community.json):
|
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](presets/catalog.community.json):
|
||||||
|
|
||||||
| Preset | Purpose | Provides | Requires | URL |
|
| Preset | Purpose | Provides | Requires | URL |
|
||||||
@@ -210,6 +243,9 @@ To build and publish your own preset, see the [Presets Publishing Guide](presets
|
|||||||
|
|
||||||
## 🚶 Community Walkthroughs
|
## 🚶 Community Walkthroughs
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community walkthroughs are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their content before following along and use at your own discretion.
|
||||||
|
|
||||||
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
|
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
|
||||||
|
|
||||||
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
|
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
|
||||||
@@ -228,9 +264,12 @@ See Spec-Driven Development in action across different scenarios with these comm
|
|||||||
|
|
||||||
## 🛠️ Community Friends
|
## 🛠️ Community Friends
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community projects listed here are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
|
||||||
|
|
||||||
Community projects that extend, visualize, or build on Spec Kit:
|
Community projects that extend, visualize, or build on Spec Kit:
|
||||||
|
|
||||||
- **[cc-sdd](https://github.com/rhuss/cc-sdd)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
|
- **[cc-spex](https://github.com/rhuss/cc-spex)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
|
||||||
|
|
||||||
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
||||||
|
|
||||||
@@ -242,7 +281,7 @@ Community projects that extend, visualize, or build on Spec Kit:
|
|||||||
| [Kiro CLI](https://kiro.dev/docs/cli/) | ✅ | Use `--ai kiro-cli` (alias: `--ai kiro`) |
|
| [Kiro CLI](https://kiro.dev/docs/cli/) | ✅ | Use `--ai kiro-cli` (alias: `--ai kiro`) |
|
||||||
| [Amp](https://ampcode.com/) | ✅ | |
|
| [Amp](https://ampcode.com/) | ✅ | |
|
||||||
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
|
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
|
||||||
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | |
|
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | Installs skills in `.claude/skills`; invoke spec-kit as `/speckit-constitution`, `/speckit-plan`, etc. |
|
||||||
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
|
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
|
||||||
| [Codex CLI](https://github.com/openai/codex) | ✅ | Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
|
| [Codex CLI](https://github.com/openai/codex) | ✅ | Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
|
||||||
| [Cursor](https://cursor.sh/) | ✅ | |
|
| [Cursor](https://cursor.sh/) | ✅ | |
|
||||||
@@ -362,8 +401,8 @@ 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
|
# Claude Code installs skills with the project by default
|
||||||
specify init my-project --ai claude --ai-skills
|
specify init my-project --ai claude
|
||||||
|
|
||||||
# Initialize in current directory with agent skills
|
# Initialize in current directory with agent skills
|
||||||
specify init --here --ai gemini --ai-skills
|
specify init --here --ai gemini --ai-skills
|
||||||
@@ -377,7 +416,11 @@ specify check
|
|||||||
|
|
||||||
### Available Slash Commands
|
### Available Slash Commands
|
||||||
|
|
||||||
After running `specify init`, your AI coding agent will have access to these slash commands for structured development.
|
After running `specify init`, your AI coding agent will have access to these structured development commands.
|
||||||
|
|
||||||
|
Most agents expose the traditional dotted slash commands shown below, like `/speckit.plan`.
|
||||||
|
|
||||||
|
Claude Code installs spec-kit as skills and invokes them as `/speckit-constitution`, `/speckit-specify`, `/speckit-plan`, `/speckit-tasks`, and `/speckit-implement`.
|
||||||
|
|
||||||
For Codex CLI, `--ai-skills` installs spec-kit as agent skills instead of slash-command prompt files. In Codex skills mode, invoke spec-kit as `$speckit-constitution`, `$speckit-specify`, `$speckit-plan`, `$speckit-tasks`, and `$speckit-implement`.
|
For Codex CLI, `--ai-skills` installs spec-kit as agent skills instead of slash-command prompt files. In Codex skills mode, invoke spec-kit as `$speckit-constitution`, `$speckit-specify`, `$speckit-plan`, `$speckit-tasks`, and `$speckit-implement`.
|
||||||
|
|
||||||
|
|||||||
66
TESTING.md
66
TESTING.md
@@ -1,8 +1,59 @@
|
|||||||
# Manual Testing Guide
|
# Testing Guide
|
||||||
|
|
||||||
|
This document is the detailed testing companion to [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||||
|
|
||||||
|
Use it for three things:
|
||||||
|
|
||||||
|
1. running quick automated checks before manual testing,
|
||||||
|
2. manually testing affected slash commands through an AI agent, and
|
||||||
|
3. capturing the results in a PR-friendly format.
|
||||||
|
|
||||||
Any change that affects a slash command's behavior requires manually testing that command through an AI agent and submitting results with the PR.
|
Any change that affects a slash command's behavior requires manually testing that command through an AI agent and submitting results with the PR.
|
||||||
|
|
||||||
## Process
|
## Recommended order
|
||||||
|
|
||||||
|
1. **Sync your environment** — install the project and test dependencies.
|
||||||
|
2. **Run focused automated checks** — especially for packaging, scaffolding, agent config, and generated-file changes.
|
||||||
|
3. **Run manual agent tests** — for any affected slash commands.
|
||||||
|
4. **Paste results into your PR** — include both command-selection reasoning and manual test results.
|
||||||
|
|
||||||
|
## Quick automated checks
|
||||||
|
|
||||||
|
Run these before manual testing when your change affects packaging, scaffolding, templates, release artifacts, or agent wiring.
|
||||||
|
|
||||||
|
### Environment setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <spec-kit-repo>
|
||||||
|
uv sync --extra test
|
||||||
|
source .venv/bin/activate # On Windows (CMD): .venv\Scripts\activate | (PowerShell): .venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generated package structure and content
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m pytest tests/test_core_pack_scaffold.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
This validates the generated files that CI-style packaging depends on, including directory layout, file names, frontmatter/TOML validity, placeholder replacement, `.specify/` path rewrites, and parity with `create-release-packages.sh`.
|
||||||
|
|
||||||
|
### Agent configuration and release wiring consistency
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m pytest tests/test_agent_config_consistency.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this when you change agent metadata, release scripts, context update scripts, or artifact naming.
|
||||||
|
|
||||||
|
### Optional single-agent packaging spot check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
AGENTS=copilot SCRIPTS=sh ./.github/workflows/scripts/create-release-packages.sh v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect `.genreleases/sdd-copilot-package-sh/` and the matching ZIP in `.genreleases/` when you want to review the exact packaged output for one agent/script combination.
|
||||||
|
|
||||||
|
## Manual testing process
|
||||||
|
|
||||||
1. **Identify affected commands** — use the [prompt below](#determining-which-tests-to-run) to have your agent analyze your changed files and determine which commands need testing.
|
1. **Identify affected commands** — use the [prompt below](#determining-which-tests-to-run) to have your agent analyze your changed files and determine which commands need testing.
|
||||||
2. **Set up a test project** — scaffold from your local branch (see [Setup](#setup)).
|
2. **Set up a test project** — scaffold from your local branch (see [Setup](#setup)).
|
||||||
@@ -13,19 +64,22 @@ Any change that affects a slash command's behavior requires manually testing tha
|
|||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install the CLI from your local branch
|
# Install the project and test dependencies from your local branch
|
||||||
cd <spec-kit-repo>
|
cd <spec-kit-repo>
|
||||||
uv venv .venv
|
uv sync --extra test
|
||||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
source .venv/bin/activate # On Windows (CMD): .venv\Scripts\activate | (PowerShell): .venv\Scripts\Activate.ps1
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
|
# Ensure the `specify` binary in this environment points at your working tree so the agent runs the branch you're testing.
|
||||||
|
|
||||||
# Initialize a test project using your local changes
|
# Initialize a test project using your local changes
|
||||||
specify init /tmp/speckit-test --ai <agent> --offline
|
uv run specify init /tmp/speckit-test --ai <agent> --offline
|
||||||
cd /tmp/speckit-test
|
cd /tmp/speckit-test
|
||||||
|
|
||||||
# Open in your agent
|
# Open in your agent
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you are testing the packaged output rather than the live source tree, create a local release package first as described in [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||||
|
|
||||||
## Reporting results
|
## Reporting results
|
||||||
|
|
||||||
Paste this into your PR:
|
Paste this into your PR:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ provides:
|
|||||||
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
|
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
|
||||||
file: string # Required, relative path to command file
|
file: string # Required, relative path to command file
|
||||||
description: string # Required
|
description: string # Required
|
||||||
aliases: [string] # Optional, array of alternate names
|
aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
|
||||||
|
|
||||||
config: # Optional, array of config files
|
config: # Optional, array of config files
|
||||||
- name: string # Config file name
|
- name: string # Config file name
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ provides:
|
|||||||
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
|
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
|
||||||
file: "commands/hello.md"
|
file: "commands/hello.md"
|
||||||
description: "Say hello"
|
description: "Say hello"
|
||||||
aliases: ["speckit.hello"] # Optional aliases
|
aliases: ["speckit.my-ext.hi"] # Optional aliases, same pattern
|
||||||
|
|
||||||
config: # Optional: Config files
|
config: # Optional: Config files
|
||||||
- name: "my-ext-config.yml"
|
- name: "my-ext-config.yml"
|
||||||
@@ -186,7 +186,7 @@ What the extension provides.
|
|||||||
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
|
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
|
||||||
- `file`: Path to command file (relative to extension root)
|
- `file`: Path to command file (relative to extension root)
|
||||||
- `description`: Command description (optional)
|
- `description`: Command description (optional)
|
||||||
- `aliases`: Alternative command names (optional, array)
|
- `aliases`: Alternative command names (optional, array; each must match `speckit.{ext-id}.{command}`)
|
||||||
|
|
||||||
### Optional Fields
|
### Optional Fields
|
||||||
|
|
||||||
@@ -514,7 +514,7 @@ zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/
|
|||||||
Users install with:
|
Users install with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 3: Community Reference Catalog
|
### Option 3: Community Reference Catalog
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ Test that users can install from your release:
|
|||||||
specify extension add --dev /path/to/your-extension
|
specify extension add --dev /path/to/your-extension
|
||||||
|
|
||||||
# Test from GitHub archive
|
# Test from GitHub archive
|
||||||
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ This will:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From GitHub release
|
# From GitHub release
|
||||||
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install from Local Directory (Development)
|
### Install from Local Directory (Development)
|
||||||
@@ -214,8 +214,8 @@ Extensions add commands that appear in your AI agent (Claude Code):
|
|||||||
# In Claude Code
|
# In Claude Code
|
||||||
> /speckit.jira.specstoissues
|
> /speckit.jira.specstoissues
|
||||||
|
|
||||||
# Or use short alias (if provided)
|
# Or use a namespaced alias (if provided)
|
||||||
> /speckit.specstoissues
|
> /speckit.jira.sync
|
||||||
```
|
```
|
||||||
|
|
||||||
### Extension Configuration
|
### Extension Configuration
|
||||||
@@ -737,7 +737,7 @@ You can still install extensions not in your catalog using `--from`:
|
|||||||
specify extension add jira
|
specify extension add jira
|
||||||
|
|
||||||
# Direct URL (bypasses catalog)
|
# Direct URL (bypasses catalog)
|
||||||
specify extension add --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
|
||||||
|
|
||||||
# Local development
|
# Local development
|
||||||
specify extension add --dev /path/to/extension
|
specify extension add --dev /path/to/extension
|
||||||
@@ -807,7 +807,7 @@ specify extension add --dev /path/to/extension
|
|||||||
2. Install older version of extension:
|
2. Install older version of extension:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension add --from https://github.com/org/ext/archive/v1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/org/ext/archive/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### MCP Tool Not Available
|
### MCP Tool Not Available
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ specify extension search # Now uses your organization's catalog instead of the
|
|||||||
|
|
||||||
### Community Reference Catalog (`catalog.community.json`)
|
### Community Reference Catalog (`catalog.community.json`)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. Review extension source code before installation and use at your own discretion.
|
||||||
|
|
||||||
- **Purpose**: Browse available community-contributed extensions
|
- **Purpose**: Browse available community-contributed extensions
|
||||||
- **Status**: Active - contains extensions submitted by the community
|
- **Status**: Active - contains extensions submitted by the community
|
||||||
- **Location**: `extensions/catalog.community.json`
|
- **Location**: `extensions/catalog.community.json`
|
||||||
@@ -59,7 +62,7 @@ Populate your `catalog.json` with approved extensions:
|
|||||||
Skip catalog curation - team members install directly using URLs:
|
Skip catalog curation - team members install directly using URLs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
specify extension add <extension-name> --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
**Benefits**: Quick for one-off testing or private extensions
|
**Benefits**: Quick for one-off testing or private extensions
|
||||||
@@ -68,6 +71,11 @@ specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/ta
|
|||||||
|
|
||||||
## Available Community Extensions
|
## Available Community Extensions
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
|
||||||
|
|
||||||
|
🔍 **Browse and search community extensions on the [Community Extensions website](https://speckit-community.github.io/extensions/).**
|
||||||
|
|
||||||
See the [Community Extensions](../README.md#-community-extensions) section in the main README for the full list of available community-contributed extensions.
|
See the [Community Extensions](../README.md#-community-extensions) section in the main README for the full list of available community-contributed extensions.
|
||||||
|
|
||||||
For the raw catalog data, see [`catalog.community.json`](catalog.community.json).
|
For the raw catalog data, see [`catalog.community.json`](catalog.community.json).
|
||||||
@@ -108,7 +116,7 @@ specify extension search # See what's in your catalog
|
|||||||
specify extension add <extension-name> # Install by name
|
specify extension add <extension-name> # Install by name
|
||||||
|
|
||||||
# Direct from URL (bypasses catalog)
|
# Direct from URL (bypasses catalog)
|
||||||
specify extension add --from https://github.com/<org>/<repo>/archive/refs/tags/<version>.zip
|
specify extension add <extension-name> --from https://github.com/<org>/<repo>/archive/refs/tags/<version>.zip
|
||||||
|
|
||||||
# List installed extensions
|
# List installed extensions
|
||||||
specify extension list
|
specify extension list
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ provides:
|
|||||||
- name: "speckit.jira.specstoissues"
|
- name: "speckit.jira.specstoissues"
|
||||||
file: "commands/specstoissues.md"
|
file: "commands/specstoissues.md"
|
||||||
description: "Create Jira hierarchy from spec and tasks"
|
description: "Create Jira hierarchy from spec and tasks"
|
||||||
aliases: ["speckit.specstoissues"] # Alternate names
|
aliases: ["speckit.jira.sync"] # Alternate names
|
||||||
|
|
||||||
- name: "speckit.jira.discover-fields"
|
- name: "speckit.jira.discover-fields"
|
||||||
file: "commands/discover-fields.md"
|
file: "commands/discover-fields.md"
|
||||||
@@ -1517,7 +1517,7 @@ specify extension add github-projects
|
|||||||
/speckit.github.taskstoissues
|
/speckit.github.taskstoissues
|
||||||
```
|
```
|
||||||
|
|
||||||
**Compatibility shim** (if needed):
|
**Migration alias** (if needed):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# extension.yml
|
# extension.yml
|
||||||
@@ -1525,10 +1525,10 @@ provides:
|
|||||||
commands:
|
commands:
|
||||||
- name: "speckit.github.taskstoissues"
|
- name: "speckit.github.taskstoissues"
|
||||||
file: "commands/taskstoissues.md"
|
file: "commands/taskstoissues.md"
|
||||||
aliases: ["speckit.taskstoissues"] # Backward compatibility
|
aliases: ["speckit.github.sync-taskstoissues"] # Alternate namespaced entry point
|
||||||
```
|
```
|
||||||
|
|
||||||
AI agent registers both names, so old scripts work.
|
AI agents register both names, so callers can migrate to the alternate alias without relying on deprecated global shortcuts like `/speckit.taskstoissues`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-19T12:08:20Z",
|
"updated_at": "2026-04-01T00:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"aide": {
|
"aide": {
|
||||||
@@ -167,50 +167,6 @@
|
|||||||
"created_at": "2026-02-22T00:00:00Z",
|
"created_at": "2026-02-22T00:00:00Z",
|
||||||
"updated_at": "2026-02-22T00:00:00Z"
|
"updated_at": "2026-02-22T00:00:00Z"
|
||||||
},
|
},
|
||||||
"cognitive-squad": {
|
|
||||||
"name": "Cognitive Squad",
|
|
||||||
"id": "cognitive-squad",
|
|
||||||
"description": "Multi-agent cognitive system with Triadic Model: understanding, internalization, application — with quality gates, backpropagation verification, and self-healing",
|
|
||||||
"author": "Testimonial",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"download_url": "https://github.com/Testimonial/cognitive-squad/archive/refs/tags/v0.1.0.zip",
|
|
||||||
"repository": "https://github.com/Testimonial/cognitive-squad",
|
|
||||||
"homepage": "https://github.com/Testimonial/cognitive-squad",
|
|
||||||
"documentation": "https://github.com/Testimonial/cognitive-squad/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/Testimonial/cognitive-squad/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.3.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "understanding",
|
|
||||||
"version": ">=3.4.0",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "spec-kit-reverse-eng",
|
|
||||||
"version": ">=1.0.0",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 10,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"ai-agents",
|
|
||||||
"cognitive",
|
|
||||||
"full-lifecycle",
|
|
||||||
"verification",
|
|
||||||
"multi-agent"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-16T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-18T00:00:00Z"
|
|
||||||
},
|
|
||||||
"conduct": {
|
"conduct": {
|
||||||
"name": "Conduct Extension",
|
"name": "Conduct Extension",
|
||||||
"id": "conduct",
|
"id": "conduct",
|
||||||
@@ -241,8 +197,38 @@
|
|||||||
"created_at": "2026-03-19T12:08:20Z",
|
"created_at": "2026-03-19T12:08:20Z",
|
||||||
"updated_at": "2026-03-19T12:08:20Z"
|
"updated_at": "2026-03-19T12:08:20Z"
|
||||||
},
|
},
|
||||||
|
"critique": {
|
||||||
|
"name": "Spec Critique Extension",
|
||||||
|
"id": "critique",
|
||||||
|
"description": "Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives.",
|
||||||
|
"author": "arunt14",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/arunt14/spec-kit-critique/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/arunt14/spec-kit-critique",
|
||||||
|
"homepage": "https://github.com/arunt14/spec-kit-critique",
|
||||||
|
"documentation": "https://github.com/arunt14/spec-kit-critique/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/arunt14/spec-kit-critique/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"docs",
|
||||||
|
"review",
|
||||||
|
"planning"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
|
},
|
||||||
"docguard": {
|
"docguard": {
|
||||||
"name": "DocGuard \u2014 CDD Enforcement",
|
"name": "DocGuard — CDD Enforcement",
|
||||||
"id": "docguard",
|
"id": "docguard",
|
||||||
"description": "Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies.",
|
"description": "Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies.",
|
||||||
"author": "raccioly",
|
"author": "raccioly",
|
||||||
@@ -345,6 +331,38 @@
|
|||||||
"created_at": "2026-03-18T00:00:00Z",
|
"created_at": "2026-03-18T00:00:00Z",
|
||||||
"updated_at": "2026-03-18T00:00:00Z"
|
"updated_at": "2026-03-18T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"fix-findings": {
|
||||||
|
"name": "Fix Findings",
|
||||||
|
"id": "fix-findings",
|
||||||
|
"description": "Automated analyze-fix-reanalyze loop that resolves spec findings until clean.",
|
||||||
|
"author": "Quratulain-bilal",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/Quratulain-bilal/spec-kit-fix-findings/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/Quratulain-bilal/spec-kit-fix-findings",
|
||||||
|
"homepage": "https://github.com/Quratulain-bilal/spec-kit-fix-findings",
|
||||||
|
"documentation": "https://github.com/Quratulain-bilal/spec-kit-fix-findings/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/Quratulain-bilal/spec-kit-fix-findings/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"code",
|
||||||
|
"analysis",
|
||||||
|
"quality",
|
||||||
|
"automation",
|
||||||
|
"findings"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
|
},
|
||||||
"fleet": {
|
"fleet": {
|
||||||
"name": "Fleet Orchestrator",
|
"name": "Fleet Orchestrator",
|
||||||
"id": "fleet",
|
"id": "fleet",
|
||||||
@@ -437,6 +455,327 @@
|
|||||||
"created_at": "2026-03-05T00:00:00Z",
|
"created_at": "2026-03-05T00:00:00Z",
|
||||||
"updated_at": "2026-03-05T00:00:00Z"
|
"updated_at": "2026-03-05T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"learn": {
|
||||||
|
"name": "Learning Extension",
|
||||||
|
"id": "learn",
|
||||||
|
"description": "Generate educational guides from implementations and enhance clarifications with mentoring context.",
|
||||||
|
"author": "Vianca Martinez",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/imviancagrace/spec-kit-learn/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/imviancagrace/spec-kit-learn",
|
||||||
|
"homepage": "https://github.com/imviancagrace/spec-kit-learn",
|
||||||
|
"documentation": "https://github.com/imviancagrace/spec-kit-learn/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/imviancagrace/spec-kit-learn/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"learning",
|
||||||
|
"education",
|
||||||
|
"mentoring",
|
||||||
|
"knowledge-transfer"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-17T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-17T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa": {
|
||||||
|
"name": "MAQA — Multi-Agent & Quality Assurance",
|
||||||
|
"id": "maqa",
|
||||||
|
"description": "Coordinator → feature → QA agent workflow with parallel worktree-based implementation. Language-agnostic. Auto-detects installed board plugins (Trello, Linear, GitHub Projects, Jira, Azure DevOps). Optional CI gate.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-ext/releases/download/maqa-v0.1.3/maqa.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-ext",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-ext",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-ext/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-ext/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 4,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"multi-agent",
|
||||||
|
"orchestration",
|
||||||
|
"quality-assurance",
|
||||||
|
"workflow",
|
||||||
|
"parallel",
|
||||||
|
"tdd"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-26T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-27T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa-azure-devops": {
|
||||||
|
"name": "MAQA Azure DevOps Integration",
|
||||||
|
"id": "maqa-azure-devops",
|
||||||
|
"description": "Azure DevOps Boards integration for the MAQA extension. Populates work items from specs, moves User Stories across columns as features progress, real-time Task child ticking.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/releases/download/maqa-azure-devops-v0.1.0/maqa-azure-devops.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"azure-devops",
|
||||||
|
"project-management",
|
||||||
|
"multi-agent",
|
||||||
|
"maqa",
|
||||||
|
"kanban"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-27T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-27T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa-ci": {
|
||||||
|
"name": "MAQA CI/CD Gate",
|
||||||
|
"id": "maqa-ci",
|
||||||
|
"description": "CI/CD pipeline gate for the MAQA extension. Auto-detects GitHub Actions, CircleCI, GitLab CI, and Bitbucket Pipelines. Blocks QA handoff until pipeline is green.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-ci/releases/download/maqa-ci-v0.1.0/maqa-ci.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-ci",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-ci",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-ci/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-ci/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"ci-cd",
|
||||||
|
"github-actions",
|
||||||
|
"circleci",
|
||||||
|
"gitlab-ci",
|
||||||
|
"quality-gate",
|
||||||
|
"maqa"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-27T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-27T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa-github-projects": {
|
||||||
|
"name": "MAQA GitHub Projects Integration",
|
||||||
|
"id": "maqa-github-projects",
|
||||||
|
"description": "GitHub Projects v2 integration for the MAQA extension. Populates draft issues from specs, moves items across Status columns as features progress, real-time task list ticking.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/releases/download/maqa-github-projects-v0.1.0/maqa-github-projects.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-github-projects",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-github-projects",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"github-projects",
|
||||||
|
"project-management",
|
||||||
|
"multi-agent",
|
||||||
|
"maqa",
|
||||||
|
"kanban"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-27T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-27T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa-jira": {
|
||||||
|
"name": "MAQA Jira Integration",
|
||||||
|
"id": "maqa-jira",
|
||||||
|
"description": "Jira integration for the MAQA extension. Populates Stories from specs, moves issues across board columns as features progress, real-time Subtask ticking.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-jira/releases/download/maqa-jira-v0.1.0/maqa-jira.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-jira",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-jira",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-jira/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-jira/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"jira",
|
||||||
|
"project-management",
|
||||||
|
"multi-agent",
|
||||||
|
"maqa",
|
||||||
|
"kanban"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-27T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-27T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa-linear": {
|
||||||
|
"name": "MAQA Linear Integration",
|
||||||
|
"id": "maqa-linear",
|
||||||
|
"description": "Linear integration for the MAQA extension. Populates issues from specs, moves items across workflow states as features progress, real-time sub-issue ticking.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-linear/releases/download/maqa-linear-v0.1.0/maqa-linear.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-linear",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-linear",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-linear/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-linear/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"linear",
|
||||||
|
"project-management",
|
||||||
|
"multi-agent",
|
||||||
|
"maqa",
|
||||||
|
"kanban"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-27T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-27T00:00:00Z"
|
||||||
|
},
|
||||||
|
"maqa-trello": {
|
||||||
|
"name": "MAQA Trello Integration",
|
||||||
|
"id": "maqa-trello",
|
||||||
|
"description": "Trello board integration for the MAQA extension. Populates board from specs, moves cards between lists as features progress, real-time checklist ticking.",
|
||||||
|
"author": "GenieRobot",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"download_url": "https://github.com/GenieRobot/spec-kit-maqa-trello/releases/download/maqa-trello-v0.1.1/maqa-trello.zip",
|
||||||
|
"repository": "https://github.com/GenieRobot/spec-kit-maqa-trello",
|
||||||
|
"homepage": "https://github.com/GenieRobot/spec-kit-maqa-trello",
|
||||||
|
"documentation": "https://github.com/GenieRobot/spec-kit-maqa-trello/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/GenieRobot/spec-kit-maqa-trello/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.3.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 2,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"trello",
|
||||||
|
"project-management",
|
||||||
|
"multi-agent",
|
||||||
|
"maqa",
|
||||||
|
"kanban"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-26T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-26T00:00:00Z"
|
||||||
|
},
|
||||||
|
"onboard": {
|
||||||
|
"name": "Onboard",
|
||||||
|
"id": "onboard",
|
||||||
|
"description": "Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step.",
|
||||||
|
"author": "Rafael Sales",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"download_url": "https://github.com/dmux/spec-kit-onboard/archive/refs/tags/v2.1.0.zip",
|
||||||
|
"repository": "https://github.com/dmux/spec-kit-onboard",
|
||||||
|
"homepage": "https://github.com/dmux/spec-kit-onboard",
|
||||||
|
"documentation": "https://github.com/dmux/spec-kit-onboard/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/dmux/spec-kit-onboard/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 7,
|
||||||
|
"hooks": 3
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"onboarding",
|
||||||
|
"learning",
|
||||||
|
"mentoring",
|
||||||
|
"developer-experience",
|
||||||
|
"gamification",
|
||||||
|
"knowledge-transfer"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-26T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-26T00:00:00Z"
|
||||||
|
},
|
||||||
|
"plan-review-gate": {
|
||||||
|
"name": "Plan Review Gate",
|
||||||
|
"id": "plan-review-gate",
|
||||||
|
"description": "Require spec.md and plan.md to be merged via MR/PR before allowing task generation",
|
||||||
|
"author": "luno",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/luno/spec-kit-plan-review-gate/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/luno/spec-kit-plan-review-gate",
|
||||||
|
"homepage": "https://github.com/luno/spec-kit-plan-review-gate",
|
||||||
|
"documentation": "https://github.com/luno/spec-kit-plan-review-gate/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/luno/spec-kit-plan-review-gate/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"review",
|
||||||
|
"quality",
|
||||||
|
"workflow",
|
||||||
|
"gate"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-27T08:22:30Z",
|
||||||
|
"updated_at": "2026-03-27T08:22:30Z"
|
||||||
|
},
|
||||||
"presetify": {
|
"presetify": {
|
||||||
"name": "Presetify",
|
"name": "Presetify",
|
||||||
"id": "presetify",
|
"id": "presetify",
|
||||||
@@ -468,6 +807,68 @@
|
|||||||
"created_at": "2026-03-18T00:00:00Z",
|
"created_at": "2026-03-18T00:00:00Z",
|
||||||
"updated_at": "2026-03-18T00:00:00Z"
|
"updated_at": "2026-03-18T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"product-forge": {
|
||||||
|
"name": "Product Forge",
|
||||||
|
"id": "product-forge",
|
||||||
|
"description": "Full product lifecycle: research → product spec → SpecKit → implement → verify → test",
|
||||||
|
"author": "VaiYav",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"download_url": "https://github.com/VaiYav/speckit-product-forge/archive/refs/tags/v1.1.1.zip",
|
||||||
|
"repository": "https://github.com/VaiYav/speckit-product-forge",
|
||||||
|
"homepage": "https://github.com/VaiYav/speckit-product-forge",
|
||||||
|
"documentation": "https://github.com/VaiYav/speckit-product-forge/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/VaiYav/speckit-product-forge/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 10,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"process",
|
||||||
|
"research",
|
||||||
|
"product-spec",
|
||||||
|
"lifecycle",
|
||||||
|
"testing"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-28T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-28T00:00:00Z"
|
||||||
|
},
|
||||||
|
"qa": {
|
||||||
|
"name": "QA Testing Extension",
|
||||||
|
"id": "qa",
|
||||||
|
"description": "Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec.",
|
||||||
|
"author": "arunt14",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/arunt14/spec-kit-qa/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/arunt14/spec-kit-qa",
|
||||||
|
"homepage": "https://github.com/arunt14/spec-kit-qa",
|
||||||
|
"documentation": "https://github.com/arunt14/spec-kit-qa/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/arunt14/spec-kit-qa/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"code",
|
||||||
|
"testing",
|
||||||
|
"qa"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
|
},
|
||||||
"ralph": {
|
"ralph": {
|
||||||
"name": "Ralph Loop",
|
"name": "Ralph Loop",
|
||||||
"id": "ralph",
|
"id": "ralph",
|
||||||
@@ -540,6 +941,73 @@
|
|||||||
"created_at": "2026-03-14T00:00:00Z",
|
"created_at": "2026-03-14T00:00:00Z",
|
||||||
"updated_at": "2026-03-14T00:00:00Z"
|
"updated_at": "2026-03-14T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"repoindex":{
|
||||||
|
"name": "Repository Index",
|
||||||
|
"id": "repoindex",
|
||||||
|
"description": "Generate index of your repo for overview, architecuture and module",
|
||||||
|
"author": "Yiyu Liu",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/liuyiyu/spec-kit-repoindex/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/liuyiyu/spec-kit-repoindex",
|
||||||
|
"homepage": "https://github.com/liuyiyu/spec-kit-repoindex",
|
||||||
|
"documentation": "https://github.com/liuyiyu/spec-kit-repoindex/tree/main/docs",
|
||||||
|
"changelog": "https://github.com/liuyiyu/spec-kit-repoindex/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "no need",
|
||||||
|
"version": ">=1.0.0",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 3,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"utility",
|
||||||
|
"brownfield",
|
||||||
|
"analysis"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-23T13:30:00Z",
|
||||||
|
"updated_at": "2026-03-23T13:30:00Z"
|
||||||
|
},
|
||||||
|
"retro": {
|
||||||
|
"name": "Retro Extension",
|
||||||
|
"id": "retro",
|
||||||
|
"description": "Sprint retrospective analysis with metrics, spec accuracy assessment, and improvement suggestions.",
|
||||||
|
"author": "arunt14",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/arunt14/spec-kit-retro/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/arunt14/spec-kit-retro",
|
||||||
|
"homepage": "https://github.com/arunt14/spec-kit-retro",
|
||||||
|
"documentation": "https://github.com/arunt14/spec-kit-retro/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/arunt14/spec-kit-retro/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 0
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"process",
|
||||||
|
"retrospective",
|
||||||
|
"metrics"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
|
},
|
||||||
"retrospective": {
|
"retrospective": {
|
||||||
"name": "Retrospective Extension",
|
"name": "Retrospective Extension",
|
||||||
"id": "retrospective",
|
"id": "retrospective",
|
||||||
@@ -606,6 +1074,36 @@
|
|||||||
"created_at": "2026-03-06T00:00:00Z",
|
"created_at": "2026-03-06T00:00:00Z",
|
||||||
"updated_at": "2026-03-06T00:00:00Z"
|
"updated_at": "2026-03-06T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"ship": {
|
||||||
|
"name": "Ship Release Extension",
|
||||||
|
"id": "ship",
|
||||||
|
"description": "Automates release pipeline: pre-flight checks, branch sync, changelog generation, CI verification, and PR creation.",
|
||||||
|
"author": "arunt14",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/arunt14/spec-kit-ship/archive/refs/tags/v1.0.0.zip",
|
||||||
|
"repository": "https://github.com/arunt14/spec-kit-ship",
|
||||||
|
"homepage": "https://github.com/arunt14/spec-kit-ship",
|
||||||
|
"documentation": "https://github.com/arunt14/spec-kit-ship/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/arunt14/spec-kit-ship/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 1,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"process",
|
||||||
|
"release",
|
||||||
|
"automation"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
|
},
|
||||||
"speckit-utils": {
|
"speckit-utils": {
|
||||||
"name": "SDD Utilities",
|
"name": "SDD Utilities",
|
||||||
"id": "speckit-utils",
|
"id": "speckit-utils",
|
||||||
@@ -638,78 +1136,35 @@
|
|||||||
"created_at": "2026-03-18T00:00:00Z",
|
"created_at": "2026-03-18T00:00:00Z",
|
||||||
"updated_at": "2026-03-18T00:00:00Z"
|
"updated_at": "2026-03-18T00:00:00Z"
|
||||||
},
|
},
|
||||||
"sync": {
|
"staff-review": {
|
||||||
"name": "Spec Sync",
|
"name": "Staff Review Extension",
|
||||||
"id": "sync",
|
"id": "staff-review",
|
||||||
"description": "Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval.",
|
"description": "Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage.",
|
||||||
"author": "bgervin",
|
"author": "arunt14",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"download_url": "https://github.com/bgervin/spec-kit-sync/archive/refs/tags/v0.1.0.zip",
|
"download_url": "https://github.com/arunt14/spec-kit-staff-review/archive/refs/tags/v1.0.0.zip",
|
||||||
"repository": "https://github.com/bgervin/spec-kit-sync",
|
"repository": "https://github.com/arunt14/spec-kit-staff-review",
|
||||||
"homepage": "https://github.com/bgervin/spec-kit-sync",
|
"homepage": "https://github.com/arunt14/spec-kit-staff-review",
|
||||||
"documentation": "https://github.com/bgervin/spec-kit-sync/blob/main/README.md",
|
"documentation": "https://github.com/arunt14/spec-kit-staff-review/blob/main/README.md",
|
||||||
"changelog": "https://github.com/bgervin/spec-kit-sync/blob/main/CHANGELOG.md",
|
"changelog": "https://github.com/arunt14/spec-kit-staff-review/blob/main/CHANGELOG.md",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"requires": {
|
"requires": {
|
||||||
"speckit_version": ">=0.1.0"
|
"speckit_version": ">=0.1.0"
|
||||||
},
|
},
|
||||||
"provides": {
|
"provides": {
|
||||||
"commands": 5,
|
"commands": 1,
|
||||||
"hooks": 1
|
"hooks": 1
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"sync",
|
"code",
|
||||||
"drift",
|
"review",
|
||||||
"validation",
|
"quality"
|
||||||
"bidirectional",
|
|
||||||
"backfill"
|
|
||||||
],
|
],
|
||||||
"verified": false,
|
"verified": false,
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"stars": 0,
|
"stars": 0,
|
||||||
"created_at": "2026-03-02T00:00:00Z",
|
"created_at": "2026-04-01T00:00:00Z",
|
||||||
"updated_at": "2026-03-02T00:00:00Z"
|
"updated_at": "2026-04-01T00:00:00Z"
|
||||||
},
|
|
||||||
"understanding": {
|
|
||||||
"name": "Understanding",
|
|
||||||
"id": "understanding",
|
|
||||||
"description": "Automated requirements quality analysis \u2014 validates specs against IEEE/ISO standards using 31 deterministic metrics. Catches ambiguity, missing testability, and structural issues before they reach implementation. Includes experimental energy-based ambiguity detection using local LM token perplexity.",
|
|
||||||
"author": "Ladislav Bihari",
|
|
||||||
"version": "3.4.0",
|
|
||||||
"download_url": "https://github.com/Testimonial/understanding/archive/refs/tags/v3.4.0.zip",
|
|
||||||
"repository": "https://github.com/Testimonial/understanding",
|
|
||||||
"homepage": "https://github.com/Testimonial/understanding",
|
|
||||||
"documentation": "https://github.com/Testimonial/understanding/blob/main/extension/README.md",
|
|
||||||
"changelog": "https://github.com/Testimonial/understanding/blob/main/extension/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0",
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "understanding",
|
|
||||||
"version": ">=3.4.0",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 3,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"quality",
|
|
||||||
"metrics",
|
|
||||||
"requirements",
|
|
||||||
"validation",
|
|
||||||
"readability",
|
|
||||||
"IEEE-830",
|
|
||||||
"ISO-29148"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-07T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-07T00:00:00Z"
|
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"name": "Project Status",
|
"name": "Project Status",
|
||||||
@@ -743,6 +1198,81 @@
|
|||||||
"created_at": "2026-03-16T00:00:00Z",
|
"created_at": "2026-03-16T00:00:00Z",
|
||||||
"updated_at": "2026-03-16T00:00:00Z"
|
"updated_at": "2026-03-16T00:00:00Z"
|
||||||
},
|
},
|
||||||
|
"superb": {
|
||||||
|
"name": "Superpowers Bridge",
|
||||||
|
"id": "superb",
|
||||||
|
"description": "Orchestrates obra/superpowers skills within the spec-kit SDD workflow. Thin bridge commands delegate to superpowers' authoritative SKILL.md files at runtime (with graceful fallback), while bridge-original commands provide spec-kit-native value. Eight commands cover the full lifecycle: intent clarification, TDD enforcement, task review, verification, critique, systematic debugging, branch completion, and review response. Hook-bound commands fire automatically; standalone commands are invoked when needed.",
|
||||||
|
"author": "rbbtsn0w",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"download_url": "https://github.com/RbBtSn0w/spec-kit-extensions/releases/download/superpowers-bridge-v1.0.0/superpowers-bridge.zip",
|
||||||
|
"repository": "https://github.com/RbBtSn0w/spec-kit-extensions",
|
||||||
|
"homepage": "https://github.com/RbBtSn0w/spec-kit-extensions",
|
||||||
|
"documentation": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/superpowers-bridge/README.md",
|
||||||
|
"changelog": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/superpowers-bridge/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.4.3",
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "superpowers",
|
||||||
|
"version": ">=5.0.0",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 8,
|
||||||
|
"hooks": 4
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"methodology",
|
||||||
|
"tdd",
|
||||||
|
"code-review",
|
||||||
|
"workflow",
|
||||||
|
"superpowers",
|
||||||
|
"brainstorming",
|
||||||
|
"verification",
|
||||||
|
"debugging",
|
||||||
|
"branch-management"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-30T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-30T00:00:00Z"
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"name": "Spec Sync",
|
||||||
|
"id": "sync",
|
||||||
|
"description": "Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval.",
|
||||||
|
"author": "bgervin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"download_url": "https://github.com/bgervin/spec-kit-sync/archive/refs/tags/v0.1.0.zip",
|
||||||
|
"repository": "https://github.com/bgervin/spec-kit-sync",
|
||||||
|
"homepage": "https://github.com/bgervin/spec-kit-sync",
|
||||||
|
"documentation": "https://github.com/bgervin/spec-kit-sync/blob/main/README.md",
|
||||||
|
"changelog": "https://github.com/bgervin/spec-kit-sync/blob/main/CHANGELOG.md",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": {
|
||||||
|
"speckit_version": ">=0.1.0"
|
||||||
|
},
|
||||||
|
"provides": {
|
||||||
|
"commands": 5,
|
||||||
|
"hooks": 1
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"sync",
|
||||||
|
"drift",
|
||||||
|
"validation",
|
||||||
|
"bidirectional",
|
||||||
|
"backfill"
|
||||||
|
],
|
||||||
|
"verified": false,
|
||||||
|
"downloads": 0,
|
||||||
|
"stars": 0,
|
||||||
|
"created_at": "2026-03-02T00:00:00Z",
|
||||||
|
"updated_at": "2026-03-02T00:00:00Z"
|
||||||
|
},
|
||||||
"v-model": {
|
"v-model": {
|
||||||
"name": "V-Model Extension Pack",
|
"name": "V-Model Extension Pack",
|
||||||
"id": "v-model",
|
"id": "v-model",
|
||||||
@@ -775,37 +1305,6 @@
|
|||||||
"created_at": "2026-02-20T00:00:00Z",
|
"created_at": "2026-02-20T00:00:00Z",
|
||||||
"updated_at": "2026-02-22T00:00:00Z"
|
"updated_at": "2026-02-22T00:00:00Z"
|
||||||
},
|
},
|
||||||
"learn": {
|
|
||||||
"name": "Learning Extension",
|
|
||||||
"id": "learn",
|
|
||||||
"description": "Generate educational guides from implementations and enhance clarifications with mentoring context.",
|
|
||||||
"author": "Vianca Martinez",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/imviancagrace/spec-kit-learn/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/imviancagrace/spec-kit-learn",
|
|
||||||
"homepage": "https://github.com/imviancagrace/spec-kit-learn",
|
|
||||||
"documentation": "https://github.com/imviancagrace/spec-kit-learn/blob/main/README.md",
|
|
||||||
"changelog": "https://github.com/imviancagrace/spec-kit-learn/blob/main/CHANGELOG.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"commands": 2,
|
|
||||||
"hooks": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"learning",
|
|
||||||
"education",
|
|
||||||
"mentoring",
|
|
||||||
"knowledge-transfer"
|
|
||||||
],
|
|
||||||
"verified": false,
|
|
||||||
"downloads": 0,
|
|
||||||
"stars": 0,
|
|
||||||
"created_at": "2026-03-17T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-17T00:00:00Z"
|
|
||||||
},
|
|
||||||
"verify": {
|
"verify": {
|
||||||
"name": "Verify Extension",
|
"name": "Verify Extension",
|
||||||
"id": "verify",
|
"id": "verify",
|
||||||
@@ -869,5 +1368,6 @@
|
|||||||
"created_at": "2026-03-16T00:00:00Z",
|
"created_at": "2026-03-16T00:00:00Z",
|
||||||
"updated_at": "2026-03-16T00:00:00Z"
|
"updated_at": "2026-03-16T00:00:00Z"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ provides:
|
|||||||
- name: "speckit.my-extension.example"
|
- name: "speckit.my-extension.example"
|
||||||
file: "commands/example.md"
|
file: "commands/example.md"
|
||||||
description: "Example command that demonstrates functionality"
|
description: "Example command that demonstrates functionality"
|
||||||
# Optional: Add aliases for shorter command names
|
# Optional: Add aliases in the same namespaced format
|
||||||
aliases: ["speckit.example"]
|
aliases: ["speckit.my-extension.example-short"]
|
||||||
|
|
||||||
# ADD MORE COMMANDS: Copy this block for each command
|
# ADD MORE COMMANDS: Copy this block for each command
|
||||||
# - name: "speckit.my-extension.another-command"
|
# - name: "speckit.my-extension.another-command"
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ Presets **override**, they don't merge. If two presets both provide `spec-templa
|
|||||||
|
|
||||||
Presets are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
|
Presets are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List active catalogs
|
# List active catalogs
|
||||||
specify preset catalog list
|
specify preset catalog list
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.4.2"
|
version = "0.4.6.dev0"
|
||||||
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 = [
|
||||||
@@ -41,8 +41,6 @@ packages = ["src/specify_cli"]
|
|||||||
"templates/commands" = "specify_cli/core_pack/commands"
|
"templates/commands" = "specify_cli/core_pack/commands"
|
||||||
"scripts/bash" = "specify_cli/core_pack/scripts/bash"
|
"scripts/bash" = "specify_cli/core_pack/scripts/bash"
|
||||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
||||||
".github/workflows/scripts/create-release-packages.sh" = "specify_cli/core_pack/release_scripts/create-release-packages.sh"
|
|
||||||
".github/workflows/scripts/create-release-packages.ps1" = "specify_cli/core_pack/release_scripts/create-release-packages.ps1"
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ get_current_branch() {
|
|||||||
latest_timestamp="$ts"
|
latest_timestamp="$ts"
|
||||||
latest_feature=$dirname
|
latest_feature=$dirname
|
||||||
fi
|
fi
|
||||||
elif [[ "$dirname" =~ ^([0-9]{3})- ]]; then
|
elif [[ "$dirname" =~ ^([0-9]{3,})- ]]; then
|
||||||
local number=${BASH_REMATCH[1]}
|
local number=${BASH_REMATCH[1]}
|
||||||
number=$((10#$number))
|
number=$((10#$number))
|
||||||
if [[ "$number" -gt "$highest" ]]; then
|
if [[ "$number" -gt "$highest" ]]; then
|
||||||
@@ -124,9 +124,15 @@ check_feature_branch() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! "$branch" =~ ^[0-9]{3}- ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
|
# Accept sequential prefix (3+ digits) but exclude malformed timestamps
|
||||||
|
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
|
||||||
|
local is_sequential=false
|
||||||
|
if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then
|
||||||
|
is_sequential=true
|
||||||
|
fi
|
||||||
|
if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
|
||||||
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
||||||
echo "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name" >&2
|
echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -146,7 +152,7 @@ find_feature_dir_by_prefix() {
|
|||||||
local prefix=""
|
local prefix=""
|
||||||
if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
|
if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
|
||||||
prefix="${BASH_REMATCH[1]}"
|
prefix="${BASH_REMATCH[1]}"
|
||||||
elif [[ "$branch_name" =~ ^([0-9]{3})- ]]; then
|
elif [[ "$branch_name" =~ ^([0-9]{3,})- ]]; then
|
||||||
prefix="${BASH_REMATCH[1]}"
|
prefix="${BASH_REMATCH[1]}"
|
||||||
else
|
else
|
||||||
# If branch doesn't have a recognized prefix, fall back to exact match
|
# If branch doesn't have a recognized prefix, fall back to exact match
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
JSON_MODE=false
|
JSON_MODE=false
|
||||||
|
DRY_RUN=false
|
||||||
|
ALLOW_EXISTING=false
|
||||||
SHORT_NAME=""
|
SHORT_NAME=""
|
||||||
BRANCH_NUMBER=""
|
BRANCH_NUMBER=""
|
||||||
USE_TIMESTAMP=false
|
USE_TIMESTAMP=false
|
||||||
@@ -14,6 +16,12 @@ while [ $i -le $# ]; do
|
|||||||
--json)
|
--json)
|
||||||
JSON_MODE=true
|
JSON_MODE=true
|
||||||
;;
|
;;
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=true
|
||||||
|
;;
|
||||||
|
--allow-existing-branch)
|
||||||
|
ALLOW_EXISTING=true
|
||||||
|
;;
|
||||||
--short-name)
|
--short-name)
|
||||||
if [ $((i + 1)) -gt $# ]; then
|
if [ $((i + 1)) -gt $# ]; then
|
||||||
echo 'Error: --short-name requires a value' >&2
|
echo 'Error: --short-name requires a value' >&2
|
||||||
@@ -45,10 +53,12 @@ while [ $i -le $# ]; do
|
|||||||
USE_TIMESTAMP=true
|
USE_TIMESTAMP=true
|
||||||
;;
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
|
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --json Output in JSON format"
|
echo " --json Output in JSON format"
|
||||||
|
echo " --dry-run Compute branch name and paths without creating branches, directories, or files"
|
||||||
|
echo " --allow-existing-branch Switch to branch if it already exists instead of failing"
|
||||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||||
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
||||||
@@ -69,7 +79,7 @@ done
|
|||||||
|
|
||||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||||
echo "Usage: $0 [--json] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -89,9 +99,9 @@ get_highest_from_specs() {
|
|||||||
for dir in "$specs_dir"/*; do
|
for dir in "$specs_dir"/*; do
|
||||||
[ -d "$dir" ] || continue
|
[ -d "$dir" ] || continue
|
||||||
dirname=$(basename "$dir")
|
dirname=$(basename "$dir")
|
||||||
# Only match sequential prefixes (###-*), skip timestamp dirs
|
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
|
||||||
if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then
|
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
||||||
number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')
|
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
|
||||||
number=$((10#$number))
|
number=$((10#$number))
|
||||||
if [ "$number" -gt "$highest" ]; then
|
if [ "$number" -gt "$highest" ]; then
|
||||||
highest=$number
|
highest=$number
|
||||||
@@ -105,39 +115,59 @@ get_highest_from_specs() {
|
|||||||
|
|
||||||
# Function to get highest number from git branches
|
# Function to get highest number from git branches
|
||||||
get_highest_from_branches() {
|
get_highest_from_branches() {
|
||||||
|
git branch -a 2>/dev/null | sed 's/^[* ]*//; s|^remotes/[^/]*/||' | _extract_highest_number
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the highest sequential feature number from a list of ref names (one per line).
|
||||||
|
# Shared by get_highest_from_branches and get_highest_from_remote_refs.
|
||||||
|
_extract_highest_number() {
|
||||||
local highest=0
|
local highest=0
|
||||||
|
while IFS= read -r name; do
|
||||||
# Get all branches (local and remote)
|
[ -z "$name" ] && continue
|
||||||
branches=$(git branch -a 2>/dev/null || echo "")
|
if echo "$name" | grep -Eq '^[0-9]{3,}-' && ! echo "$name" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
||||||
|
number=$(echo "$name" | grep -Eo '^[0-9]+' || echo "0")
|
||||||
if [ -n "$branches" ]; then
|
number=$((10#$number))
|
||||||
while IFS= read -r branch; do
|
if [ "$number" -gt "$highest" ]; then
|
||||||
# Clean branch name: remove leading markers and remote prefixes
|
highest=$number
|
||||||
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
|
||||||
|
|
||||||
# Extract feature number if branch matches pattern ###-*
|
|
||||||
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
|
||||||
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
|
||||||
number=$((10#$number))
|
|
||||||
if [ "$number" -gt "$highest" ]; then
|
|
||||||
highest=$number
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done <<< "$branches"
|
fi
|
||||||
fi
|
done
|
||||||
|
|
||||||
echo "$highest"
|
echo "$highest"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to check existing branches (local and remote) and return next available number
|
# Function to get highest number from remote branches without fetching (side-effect-free)
|
||||||
|
get_highest_from_remote_refs() {
|
||||||
|
local highest=0
|
||||||
|
|
||||||
|
for remote in $(git remote 2>/dev/null); do
|
||||||
|
local remote_highest
|
||||||
|
remote_highest=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads "$remote" 2>/dev/null | sed 's|.*refs/heads/||' | _extract_highest_number)
|
||||||
|
if [ "$remote_highest" -gt "$highest" ]; then
|
||||||
|
highest=$remote_highest
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$highest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check existing branches (local and remote) and return next available number.
|
||||||
|
# When skip_fetch is true, queries remotes via ls-remote (read-only) instead of fetching.
|
||||||
check_existing_branches() {
|
check_existing_branches() {
|
||||||
local specs_dir="$1"
|
local specs_dir="$1"
|
||||||
|
local skip_fetch="${2:-false}"
|
||||||
|
|
||||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
if [ "$skip_fetch" = true ]; then
|
||||||
git fetch --all --prune >/dev/null 2>&1 || true
|
# Side-effect-free: query remotes via ls-remote
|
||||||
|
local highest_remote=$(get_highest_from_remote_refs)
|
||||||
# Get highest number from ALL branches (not just matching short name)
|
local highest_branch=$(get_highest_from_branches)
|
||||||
local highest_branch=$(get_highest_from_branches)
|
if [ "$highest_remote" -gt "$highest_branch" ]; then
|
||||||
|
highest_branch=$highest_remote
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
|
git fetch --all --prune >/dev/null 2>&1 || true
|
||||||
|
local highest_branch=$(get_highest_from_branches)
|
||||||
|
fi
|
||||||
|
|
||||||
# Get highest number from ALL specs (not just matching short name)
|
# Get highest number from ALL specs (not just matching short name)
|
||||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||||
@@ -174,7 +204,9 @@ fi
|
|||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
SPECS_DIR="$REPO_ROOT/specs"
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
mkdir -p "$SPECS_DIR"
|
if [ "$DRY_RUN" != true ]; then
|
||||||
|
mkdir -p "$SPECS_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
generate_branch_name() {
|
generate_branch_name() {
|
||||||
@@ -246,7 +278,14 @@ if [ "$USE_TIMESTAMP" = true ]; then
|
|||||||
else
|
else
|
||||||
# Determine branch number
|
# Determine branch number
|
||||||
if [ -z "$BRANCH_NUMBER" ]; then
|
if [ -z "$BRANCH_NUMBER" ]; then
|
||||||
if [ "$HAS_GIT" = true ]; then
|
if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then
|
||||||
|
# Dry-run: query remotes via ls-remote (side-effect-free, no fetch)
|
||||||
|
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR" true)
|
||||||
|
elif [ "$DRY_RUN" = true ]; then
|
||||||
|
# Dry-run without git: local spec dirs only
|
||||||
|
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||||
|
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||||
|
elif [ "$HAS_GIT" = true ]; then
|
||||||
# Check existing branches on remotes
|
# Check existing branches on remotes
|
||||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||||
else
|
else
|
||||||
@@ -283,53 +322,79 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
|||||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$HAS_GIT" = true ]; then
|
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||||
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||||
# Check if branch already exists
|
|
||||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
if [ "$DRY_RUN" != true ]; then
|
||||||
if [ "$USE_TIMESTAMP" = true ]; then
|
if [ "$HAS_GIT" = true ]; then
|
||||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
|
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
||||||
|
# Check if branch already exists
|
||||||
|
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||||
|
if [ "$ALLOW_EXISTING" = true ]; then
|
||||||
|
# Switch to the existing branch instead of failing
|
||||||
|
if ! git checkout "$BRANCH_NAME" 2>/dev/null; then
|
||||||
|
>&2 echo "Error: Failed to switch to existing branch '$BRANCH_NAME'. Please resolve any local changes or conflicts and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ "$USE_TIMESTAMP" = true ]; then
|
||||||
|
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
exit 1
|
fi
|
||||||
|
else
|
||||||
|
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
|
if [ ! -f "$SPEC_FILE" ]; then
|
||||||
|
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
|
||||||
|
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
|
||||||
|
cp "$TEMPLATE" "$SPEC_FILE"
|
||||||
else
|
else
|
||||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
echo "Warning: Spec template not found; created empty spec file" >&2
|
||||||
exit 1
|
touch "$SPEC_FILE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
# Inform the user how to persist the feature variable in their own shell
|
||||||
|
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
|
||||||
mkdir -p "$FEATURE_DIR"
|
|
||||||
|
|
||||||
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
|
|
||||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
|
||||||
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
|
|
||||||
cp "$TEMPLATE" "$SPEC_FILE"
|
|
||||||
else
|
|
||||||
echo "Warning: Spec template not found; created empty spec file" >&2
|
|
||||||
touch "$SPEC_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Inform the user how to persist the feature variable in their own shell
|
|
||||||
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
|
||||||
|
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
if command -v jq >/dev/null 2>&1; then
|
if command -v jq >/dev/null 2>&1; then
|
||||||
jq -cn \
|
if [ "$DRY_RUN" = true ]; then
|
||||||
--arg branch_name "$BRANCH_NAME" \
|
jq -cn \
|
||||||
--arg spec_file "$SPEC_FILE" \
|
--arg branch_name "$BRANCH_NAME" \
|
||||||
--arg feature_num "$FEATURE_NUM" \
|
--arg spec_file "$SPEC_FILE" \
|
||||||
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
|
--arg feature_num "$FEATURE_NUM" \
|
||||||
|
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
|
||||||
|
else
|
||||||
|
jq -cn \
|
||||||
|
--arg branch_name "$BRANCH_NAME" \
|
||||||
|
--arg spec_file "$SPEC_FILE" \
|
||||||
|
--arg feature_num "$FEATURE_NUM" \
|
||||||
|
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
if [ "$DRY_RUN" = true ]; then
|
||||||
|
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
||||||
|
else
|
||||||
|
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||||
echo "SPEC_FILE: $SPEC_FILE"
|
echo "SPEC_FILE: $SPEC_FILE"
|
||||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||||
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
if [ "$DRY_RUN" != true ]; then
|
||||||
|
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ AGENT_TYPE="${1:-}"
|
|||||||
# Agent-specific file paths
|
# Agent-specific file paths
|
||||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
||||||
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
||||||
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
|
COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
|
||||||
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
||||||
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
||||||
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ function Find-SpecifyRoot {
|
|||||||
|
|
||||||
# Normalize to absolute path to prevent issues with relative paths
|
# Normalize to absolute path to prevent issues with relative paths
|
||||||
# Use -LiteralPath to handle paths with wildcard characters ([, ], *, ?)
|
# Use -LiteralPath to handle paths with wildcard characters ([, ], *, ?)
|
||||||
$current = (Resolve-Path -LiteralPath $StartDir -ErrorAction SilentlyContinue)?.Path
|
$resolved = Resolve-Path -LiteralPath $StartDir -ErrorAction SilentlyContinue
|
||||||
|
$current = if ($resolved) { $resolved.Path } else { $null }
|
||||||
if (-not $current) { return $null }
|
if (-not $current) { return $null }
|
||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
@@ -82,8 +83,8 @@ function Get-CurrentBranch {
|
|||||||
$latestTimestamp = $ts
|
$latestTimestamp = $ts
|
||||||
$latestFeature = $_.Name
|
$latestFeature = $_.Name
|
||||||
}
|
}
|
||||||
} elseif ($_.Name -match '^(\d{3})-') {
|
} elseif ($_.Name -match '^(\d{3,})-') {
|
||||||
$num = [int]$matches[1]
|
$num = [long]$matches[1]
|
||||||
if ($num -gt $highest) {
|
if ($num -gt $highest) {
|
||||||
$highest = $num
|
$highest = $num
|
||||||
# Only update if no timestamp branch found yet
|
# Only update if no timestamp branch found yet
|
||||||
@@ -138,9 +139,13 @@ function Test-FeatureBranch {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Branch -notmatch '^[0-9]{3}-' -and $Branch -notmatch '^\d{8}-\d{6}-') {
|
# Accept sequential prefix (3+ digits) but exclude malformed timestamps
|
||||||
|
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
|
||||||
|
$hasMalformedTimestamp = ($Branch -match '^[0-9]{7}-[0-9]{6}-') -or ($Branch -match '^(?:\d{7}|\d{8})-\d{6}$')
|
||||||
|
$isSequential = ($Branch -match '^[0-9]{3,}-') -and (-not $hasMalformedTimestamp)
|
||||||
|
if (-not $isSequential -and $Branch -notmatch '^\d{8}-\d{6}-') {
|
||||||
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
|
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
|
||||||
Write-Output "Feature branches should be named like: 001-feature-name or 20260319-143022-feature-name"
|
Write-Output "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name"
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
return $true
|
return $true
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[switch]$Json,
|
[switch]$Json,
|
||||||
|
[switch]$AllowExistingBranch,
|
||||||
|
[switch]$DryRun,
|
||||||
[string]$ShortName,
|
[string]$ShortName,
|
||||||
[Parameter()]
|
[Parameter()]
|
||||||
[int]$Number = 0,
|
[long]$Number = 0,
|
||||||
[switch]$Timestamp,
|
[switch]$Timestamp,
|
||||||
[switch]$Help,
|
[switch]$Help,
|
||||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||||
@@ -15,10 +17,12 @@ $ErrorActionPreference = 'Stop'
|
|||||||
|
|
||||||
# Show help if requested
|
# Show help if requested
|
||||||
if ($Help) {
|
if ($Help) {
|
||||||
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Options:"
|
Write-Host "Options:"
|
||||||
Write-Host " -Json Output in JSON format"
|
Write-Host " -Json Output in JSON format"
|
||||||
|
Write-Host " -DryRun Compute branch name and paths without creating branches, directories, or files"
|
||||||
|
Write-Host " -AllowExistingBranch Switch to branch if it already exists instead of failing"
|
||||||
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
||||||
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
||||||
@@ -33,7 +37,7 @@ if ($Help) {
|
|||||||
|
|
||||||
# Check if feature description provided
|
# Check if feature description provided
|
||||||
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
||||||
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,13 +51,33 @@ if ([string]::IsNullOrWhiteSpace($featureDesc)) {
|
|||||||
|
|
||||||
function Get-HighestNumberFromSpecs {
|
function Get-HighestNumberFromSpecs {
|
||||||
param([string]$SpecsDir)
|
param([string]$SpecsDir)
|
||||||
|
|
||||||
$highest = 0
|
[long]$highest = 0
|
||||||
if (Test-Path $SpecsDir) {
|
if (Test-Path $SpecsDir) {
|
||||||
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
||||||
if ($_.Name -match '^(\d{3})-') {
|
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
|
||||||
$num = [int]$matches[1]
|
if ($_.Name -match '^(\d{3,})-' -and $_.Name -notmatch '^\d{8}-\d{6}-') {
|
||||||
if ($num -gt $highest) { $highest = $num }
|
[long]$num = 0
|
||||||
|
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
|
||||||
|
$highest = $num
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $highest
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the highest sequential feature number from a list of branch/ref names.
|
||||||
|
# Shared by Get-HighestNumberFromBranches and Get-HighestNumberFromRemoteRefs.
|
||||||
|
function Get-HighestNumberFromNames {
|
||||||
|
param([string[]]$Names)
|
||||||
|
|
||||||
|
[long]$highest = 0
|
||||||
|
foreach ($name in $Names) {
|
||||||
|
if ($name -match '^(\d{3,})-' -and $name -notmatch '^\d{8}-\d{6}-') {
|
||||||
|
[long]$num = 0
|
||||||
|
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
|
||||||
|
$highest = $num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,44 +86,68 @@ function Get-HighestNumberFromSpecs {
|
|||||||
|
|
||||||
function Get-HighestNumberFromBranches {
|
function Get-HighestNumberFromBranches {
|
||||||
param()
|
param()
|
||||||
|
|
||||||
$highest = 0
|
|
||||||
try {
|
try {
|
||||||
$branches = git branch -a 2>$null
|
$branches = git branch -a 2>$null
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0 -and $branches) {
|
||||||
foreach ($branch in $branches) {
|
$cleanNames = $branches | ForEach-Object {
|
||||||
# Clean branch name: remove leading markers and remote prefixes
|
$_.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
||||||
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
}
|
||||||
|
return Get-HighestNumberFromNames -Names $cleanNames
|
||||||
# Extract feature number if branch matches pattern ###-*
|
}
|
||||||
if ($cleanBranch -match '^(\d{3})-') {
|
} catch {
|
||||||
$num = [int]$matches[1]
|
Write-Verbose "Could not check Git branches: $_"
|
||||||
if ($num -gt $highest) { $highest = $num }
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HighestNumberFromRemoteRefs {
|
||||||
|
[long]$highest = 0
|
||||||
|
try {
|
||||||
|
$remotes = git remote 2>$null
|
||||||
|
if ($remotes) {
|
||||||
|
foreach ($remote in $remotes) {
|
||||||
|
$env:GIT_TERMINAL_PROMPT = '0'
|
||||||
|
$refs = git ls-remote --heads $remote 2>$null
|
||||||
|
$env:GIT_TERMINAL_PROMPT = $null
|
||||||
|
if ($LASTEXITCODE -eq 0 -and $refs) {
|
||||||
|
$refNames = $refs | ForEach-Object {
|
||||||
|
if ($_ -match 'refs/heads/(.+)$') { $matches[1] }
|
||||||
|
} | Where-Object { $_ }
|
||||||
|
$remoteHighest = Get-HighestNumberFromNames -Names $refNames
|
||||||
|
if ($remoteHighest -gt $highest) { $highest = $remoteHighest }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
# If git command fails, return 0
|
Write-Verbose "Could not query remote refs: $_"
|
||||||
Write-Verbose "Could not check Git branches: $_"
|
|
||||||
}
|
}
|
||||||
return $highest
|
return $highest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Return next available branch number. When SkipFetch is true, queries remotes
|
||||||
|
# via ls-remote (read-only) instead of fetching.
|
||||||
function Get-NextBranchNumber {
|
function Get-NextBranchNumber {
|
||||||
param(
|
param(
|
||||||
[string]$SpecsDir
|
[string]$SpecsDir,
|
||||||
|
[switch]$SkipFetch
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
if ($SkipFetch) {
|
||||||
try {
|
# Side-effect-free: query remotes via ls-remote
|
||||||
git fetch --all --prune 2>$null | Out-Null
|
$highestBranch = Get-HighestNumberFromBranches
|
||||||
} catch {
|
$highestRemote = Get-HighestNumberFromRemoteRefs
|
||||||
# Ignore fetch errors
|
$highestBranch = [Math]::Max($highestBranch, $highestRemote)
|
||||||
|
} else {
|
||||||
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
|
try {
|
||||||
|
git fetch --all --prune 2>$null | Out-Null
|
||||||
|
} catch {
|
||||||
|
# Ignore fetch errors
|
||||||
|
}
|
||||||
|
$highestBranch = Get-HighestNumberFromBranches
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get highest number from ALL branches (not just matching short name)
|
|
||||||
$highestBranch = Get-HighestNumberFromBranches
|
|
||||||
|
|
||||||
# Get highest number from ALL specs (not just matching short name)
|
# Get highest number from ALL specs (not just matching short name)
|
||||||
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
|
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
|
||||||
|
|
||||||
@@ -112,7 +160,7 @@ function Get-NextBranchNumber {
|
|||||||
|
|
||||||
function ConvertTo-CleanBranchName {
|
function ConvertTo-CleanBranchName {
|
||||||
param([string]$Name)
|
param([string]$Name)
|
||||||
|
|
||||||
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
||||||
}
|
}
|
||||||
# Load common functions (includes Get-RepoRoot, Test-HasGit, Resolve-Template)
|
# Load common functions (includes Get-RepoRoot, Test-HasGit, Resolve-Template)
|
||||||
@@ -127,12 +175,14 @@ $hasGit = Test-HasGit
|
|||||||
Set-Location $repoRoot
|
Set-Location $repoRoot
|
||||||
|
|
||||||
$specsDir = Join-Path $repoRoot 'specs'
|
$specsDir = Join-Path $repoRoot 'specs'
|
||||||
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
if (-not $DryRun) {
|
||||||
|
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
function Get-BranchName {
|
function Get-BranchName {
|
||||||
param([string]$Description)
|
param([string]$Description)
|
||||||
|
|
||||||
# Common stop words to filter out
|
# Common stop words to filter out
|
||||||
$stopWords = @(
|
$stopWords = @(
|
||||||
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
|
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
|
||||||
@@ -141,17 +191,17 @@ function Get-BranchName {
|
|||||||
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
|
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
|
||||||
'want', 'need', 'add', 'get', 'set'
|
'want', 'need', 'add', 'get', 'set'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert to lowercase and extract words (alphanumeric only)
|
# Convert to lowercase and extract words (alphanumeric only)
|
||||||
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
|
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
|
||||||
$words = $cleanName -split '\s+' | Where-Object { $_ }
|
$words = $cleanName -split '\s+' | Where-Object { $_ }
|
||||||
|
|
||||||
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
||||||
$meaningfulWords = @()
|
$meaningfulWords = @()
|
||||||
foreach ($word in $words) {
|
foreach ($word in $words) {
|
||||||
# Skip stop words
|
# Skip stop words
|
||||||
if ($stopWords -contains $word) { continue }
|
if ($stopWords -contains $word) { continue }
|
||||||
|
|
||||||
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
|
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
|
||||||
if ($word.Length -ge 3) {
|
if ($word.Length -ge 3) {
|
||||||
$meaningfulWords += $word
|
$meaningfulWords += $word
|
||||||
@@ -160,7 +210,7 @@ function Get-BranchName {
|
|||||||
$meaningfulWords += $word
|
$meaningfulWords += $word
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# If we have meaningful words, use first 3-4 of them
|
# If we have meaningful words, use first 3-4 of them
|
||||||
if ($meaningfulWords.Count -gt 0) {
|
if ($meaningfulWords.Count -gt 0) {
|
||||||
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
|
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
|
||||||
@@ -196,7 +246,13 @@ if ($Timestamp) {
|
|||||||
} else {
|
} else {
|
||||||
# Determine branch number
|
# Determine branch number
|
||||||
if ($Number -eq 0) {
|
if ($Number -eq 0) {
|
||||||
if ($hasGit) {
|
if ($DryRun -and $hasGit) {
|
||||||
|
# Dry-run: query remotes via ls-remote (side-effect-free, no fetch)
|
||||||
|
$Number = Get-NextBranchNumber -SpecsDir $specsDir -SkipFetch
|
||||||
|
} elseif ($DryRun) {
|
||||||
|
# Dry-run without git: local spec dirs only
|
||||||
|
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||||
|
} elseif ($hasGit) {
|
||||||
# Check existing branches on remotes
|
# Check existing branches on remotes
|
||||||
$Number = Get-NextBranchNumber -SpecsDir $specsDir
|
$Number = Get-NextBranchNumber -SpecsDir $specsDir
|
||||||
} else {
|
} else {
|
||||||
@@ -217,77 +273,94 @@ if ($branchName.Length -gt $maxBranchLength) {
|
|||||||
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
|
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
|
||||||
$prefixLength = $featureNum.Length + 1
|
$prefixLength = $featureNum.Length + 1
|
||||||
$maxSuffixLength = $maxBranchLength - $prefixLength
|
$maxSuffixLength = $maxBranchLength - $prefixLength
|
||||||
|
|
||||||
# Truncate suffix
|
# Truncate suffix
|
||||||
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
||||||
# Remove trailing hyphen if truncation created one
|
# Remove trailing hyphen if truncation created one
|
||||||
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
||||||
|
|
||||||
$originalBranchName = $branchName
|
$originalBranchName = $branchName
|
||||||
$branchName = "$featureNum-$truncatedSuffix"
|
$branchName = "$featureNum-$truncatedSuffix"
|
||||||
|
|
||||||
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
|
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
|
||||||
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
|
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
|
||||||
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
|
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hasGit) {
|
|
||||||
$branchCreated = $false
|
|
||||||
try {
|
|
||||||
git checkout -q -b $branchName 2>$null | Out-Null
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
$branchCreated = $true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Exception during git command
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $branchCreated) {
|
|
||||||
# Check if branch already exists
|
|
||||||
$existingBranch = git branch --list $branchName 2>$null
|
|
||||||
if ($existingBranch) {
|
|
||||||
if ($Timestamp) {
|
|
||||||
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
|
|
||||||
} else {
|
|
||||||
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
|
|
||||||
}
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
|
|
||||||
}
|
|
||||||
|
|
||||||
$featureDir = Join-Path $specsDir $branchName
|
$featureDir = Join-Path $specsDir $branchName
|
||||||
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
|
||||||
|
|
||||||
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
|
|
||||||
$specFile = Join-Path $featureDir 'spec.md'
|
$specFile = Join-Path $featureDir 'spec.md'
|
||||||
if ($template -and (Test-Path $template)) {
|
|
||||||
Copy-Item $template $specFile -Force
|
|
||||||
} else {
|
|
||||||
New-Item -ItemType File -Path $specFile | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set the SPECIFY_FEATURE environment variable for the current session
|
if (-not $DryRun) {
|
||||||
$env:SPECIFY_FEATURE = $branchName
|
if ($hasGit) {
|
||||||
|
$branchCreated = $false
|
||||||
|
try {
|
||||||
|
git checkout -q -b $branchName 2>$null | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
$branchCreated = $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# Exception during git command
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $branchCreated) {
|
||||||
|
# Check if branch already exists
|
||||||
|
$existingBranch = git branch --list $branchName 2>$null
|
||||||
|
if ($existingBranch) {
|
||||||
|
if ($AllowExistingBranch) {
|
||||||
|
# Switch to the existing branch instead of failing
|
||||||
|
git checkout -q $branchName 2>$null | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Error: Branch '$branchName' exists but could not be checked out. Resolve any uncommitted changes or conflicts and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} elseif ($Timestamp) {
|
||||||
|
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
|
||||||
|
exit 1
|
||||||
|
} else {
|
||||||
|
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
||||||
|
|
||||||
|
if (-not (Test-Path -PathType Leaf $specFile)) {
|
||||||
|
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
|
||||||
|
if ($template -and (Test-Path $template)) {
|
||||||
|
Copy-Item $template $specFile -Force
|
||||||
|
} else {
|
||||||
|
New-Item -ItemType File -Path $specFile -Force | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||||
|
$env:SPECIFY_FEATURE = $branchName
|
||||||
|
}
|
||||||
|
|
||||||
if ($Json) {
|
if ($Json) {
|
||||||
$obj = [PSCustomObject]@{
|
$obj = [PSCustomObject]@{
|
||||||
BRANCH_NAME = $branchName
|
BRANCH_NAME = $branchName
|
||||||
SPEC_FILE = $specFile
|
SPEC_FILE = $specFile
|
||||||
FEATURE_NUM = $featureNum
|
FEATURE_NUM = $featureNum
|
||||||
HAS_GIT = $hasGit
|
HAS_GIT = $hasGit
|
||||||
}
|
}
|
||||||
|
if ($DryRun) {
|
||||||
|
$obj | Add-Member -NotePropertyName 'DRY_RUN' -NotePropertyValue $true
|
||||||
|
}
|
||||||
$obj | ConvertTo-Json -Compress
|
$obj | ConvertTo-Json -Compress
|
||||||
} else {
|
} else {
|
||||||
Write-Output "BRANCH_NAME: $branchName"
|
Write-Output "BRANCH_NAME: $branchName"
|
||||||
Write-Output "SPEC_FILE: $specFile"
|
Write-Output "SPEC_FILE: $specFile"
|
||||||
Write-Output "FEATURE_NUM: $featureNum"
|
Write-Output "FEATURE_NUM: $featureNum"
|
||||||
Write-Output "HAS_GIT: $hasGit"
|
Write-Output "HAS_GIT: $hasGit"
|
||||||
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
if (-not $DryRun) {
|
||||||
|
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ $NEW_PLAN = $IMPL_PLAN
|
|||||||
# Agent file paths
|
# Agent file paths
|
||||||
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
|
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
|
||||||
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
|
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
|
||||||
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md'
|
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/copilot-instructions.md'
|
||||||
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
|
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
|
||||||
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
|
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
|
||||||
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,23 @@ from pathlib import Path
|
|||||||
from typing import Dict, List, Any
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def _build_agent_configs() -> dict[str, Any]:
|
||||||
|
"""Derive CommandRegistrar.AGENT_CONFIGS from INTEGRATION_REGISTRY."""
|
||||||
|
from specify_cli.integrations import INTEGRATION_REGISTRY
|
||||||
|
configs: dict[str, dict[str, Any]] = {}
|
||||||
|
for key, integration in INTEGRATION_REGISTRY.items():
|
||||||
|
if key == "generic":
|
||||||
|
continue
|
||||||
|
if integration.registrar_config:
|
||||||
|
configs[key] = dict(integration.registrar_config)
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
class CommandRegistrar:
|
class CommandRegistrar:
|
||||||
"""Handles registration of commands with AI agents.
|
"""Handles registration of commands with AI agents.
|
||||||
|
|
||||||
@@ -21,147 +35,26 @@ class CommandRegistrar:
|
|||||||
and companion files (e.g. Copilot .prompt.md).
|
and companion files (e.g. Copilot .prompt.md).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Agent configurations with directory, format, and argument placeholder
|
# Derived from INTEGRATION_REGISTRY — single source of truth.
|
||||||
AGENT_CONFIGS = {
|
# Populated lazily via _ensure_configs() on first use.
|
||||||
"claude": {
|
AGENT_CONFIGS: dict[str, dict[str, Any]] = {}
|
||||||
"dir": ".claude/commands",
|
_configs_loaded: bool = False
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
def __init__(self) -> None:
|
||||||
"extension": ".md"
|
self._ensure_configs()
|
||||||
},
|
|
||||||
"gemini": {
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
"dir": ".gemini/commands",
|
super().__init_subclass__(**kwargs)
|
||||||
"format": "toml",
|
cls._ensure_configs()
|
||||||
"args": "{{args}}",
|
|
||||||
"extension": ".toml"
|
@classmethod
|
||||||
},
|
def _ensure_configs(cls) -> None:
|
||||||
"copilot": {
|
if not cls._configs_loaded:
|
||||||
"dir": ".github/agents",
|
try:
|
||||||
"format": "markdown",
|
cls.AGENT_CONFIGS = _build_agent_configs()
|
||||||
"args": "$ARGUMENTS",
|
cls._configs_loaded = True
|
||||||
"extension": ".agent.md"
|
except ImportError:
|
||||||
},
|
pass # Circular import during module init; retry on next access
|
||||||
"cursor": {
|
|
||||||
"dir": ".cursor/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"qwen": {
|
|
||||||
"dir": ".qwen/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"opencode": {
|
|
||||||
"dir": ".opencode/command",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"codex": {
|
|
||||||
"dir": ".agents/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md",
|
|
||||||
},
|
|
||||||
"windsurf": {
|
|
||||||
"dir": ".windsurf/workflows",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"junie": {
|
|
||||||
"dir": ".junie/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"kilocode": {
|
|
||||||
"dir": ".kilocode/workflows",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"auggie": {
|
|
||||||
"dir": ".augment/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"roo": {
|
|
||||||
"dir": ".roo/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"codebuddy": {
|
|
||||||
"dir": ".codebuddy/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"qodercli": {
|
|
||||||
"dir": ".qoder/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"kiro-cli": {
|
|
||||||
"dir": ".kiro/prompts",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"pi": {
|
|
||||||
"dir": ".pi/prompts",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"amp": {
|
|
||||||
"dir": ".agents/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"shai": {
|
|
||||||
"dir": ".shai/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"tabnine": {
|
|
||||||
"dir": ".tabnine/agent/commands",
|
|
||||||
"format": "toml",
|
|
||||||
"args": "{{args}}",
|
|
||||||
"extension": ".toml"
|
|
||||||
},
|
|
||||||
"bob": {
|
|
||||||
"dir": ".bob/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"kimi": {
|
|
||||||
"dir": ".kimi/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md",
|
|
||||||
},
|
|
||||||
"trae": {
|
|
||||||
"dir": ".trae/rules",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
},
|
|
||||||
"iflow": {
|
|
||||||
"dir": ".iflow/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_frontmatter(content: str) -> tuple[dict, str]:
|
def parse_frontmatter(content: str) -> tuple[dict, str]:
|
||||||
@@ -211,24 +104,52 @@ class CommandRegistrar:
|
|||||||
return f"---\n{yaml_str}---\n"
|
return f"---\n{yaml_str}---\n"
|
||||||
|
|
||||||
def _adjust_script_paths(self, frontmatter: dict) -> dict:
|
def _adjust_script_paths(self, frontmatter: dict) -> dict:
|
||||||
"""Adjust script paths from extension-relative to repo-relative.
|
"""Normalize script paths in frontmatter to generated project locations.
|
||||||
|
|
||||||
|
Rewrites known repo-relative and top-level script paths under the
|
||||||
|
`scripts` and `agent_scripts` keys (for example `../../scripts/`,
|
||||||
|
`../../templates/`, `../../memory/`, `scripts/`, `templates/`, and
|
||||||
|
`memory/`) to the `.specify/...` paths used in generated projects.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
frontmatter: Frontmatter dictionary
|
frontmatter: Frontmatter dictionary
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Modified frontmatter with adjusted paths
|
Modified frontmatter with normalized project paths
|
||||||
"""
|
"""
|
||||||
|
frontmatter = deepcopy(frontmatter)
|
||||||
|
|
||||||
for script_key in ("scripts", "agent_scripts"):
|
for script_key in ("scripts", "agent_scripts"):
|
||||||
scripts = frontmatter.get(script_key)
|
scripts = frontmatter.get(script_key)
|
||||||
if not isinstance(scripts, dict):
|
if not isinstance(scripts, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for key, script_path in scripts.items():
|
for key, script_path in scripts.items():
|
||||||
if isinstance(script_path, str) and script_path.startswith("../../scripts/"):
|
if isinstance(script_path, str):
|
||||||
scripts[key] = f".specify/scripts/{script_path[14:]}"
|
scripts[key] = self.rewrite_project_relative_paths(script_path)
|
||||||
return frontmatter
|
return frontmatter
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rewrite_project_relative_paths(text: str) -> str:
|
||||||
|
"""Rewrite repo-relative paths to their generated project locations."""
|
||||||
|
if not isinstance(text, str) or not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
for old, new in (
|
||||||
|
("../../memory/", ".specify/memory/"),
|
||||||
|
("../../scripts/", ".specify/scripts/"),
|
||||||
|
("../../templates/", ".specify/templates/"),
|
||||||
|
):
|
||||||
|
text = text.replace(old, new)
|
||||||
|
|
||||||
|
# Only rewrite top-level style references so extension-local paths like
|
||||||
|
# ".specify/extensions/<ext>/scripts/..." remain intact.
|
||||||
|
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?memory/', r"\1.specify/memory/", text)
|
||||||
|
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?scripts/', r"\1.specify/scripts/", text)
|
||||||
|
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?templates/', r"\1.specify/templates/", text)
|
||||||
|
|
||||||
|
return text.replace(".specify/.specify/", ".specify/").replace(".specify.specify/", ".specify/")
|
||||||
|
|
||||||
def render_markdown_command(
|
def render_markdown_command(
|
||||||
self,
|
self,
|
||||||
frontmatter: dict,
|
frontmatter: dict,
|
||||||
@@ -277,9 +198,25 @@ class CommandRegistrar:
|
|||||||
toml_lines.append(f"# Source: {source_id}")
|
toml_lines.append(f"# Source: {source_id}")
|
||||||
toml_lines.append("")
|
toml_lines.append("")
|
||||||
|
|
||||||
toml_lines.append('prompt = """')
|
# Keep TOML output valid even when body contains triple-quote delimiters.
|
||||||
toml_lines.append(body)
|
# Prefer multiline forms, then fall back to escaped basic string.
|
||||||
toml_lines.append('"""')
|
if '"""' not in body:
|
||||||
|
toml_lines.append('prompt = """')
|
||||||
|
toml_lines.append(body)
|
||||||
|
toml_lines.append('"""')
|
||||||
|
elif "'''" not in body:
|
||||||
|
toml_lines.append("prompt = '''")
|
||||||
|
toml_lines.append(body)
|
||||||
|
toml_lines.append("'''")
|
||||||
|
else:
|
||||||
|
escaped_body = (
|
||||||
|
body.replace("\\", "\\\\")
|
||||||
|
.replace('"', '\\"')
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
)
|
||||||
|
toml_lines.append(f'prompt = "{escaped_body}"')
|
||||||
|
|
||||||
return "\n".join(toml_lines)
|
return "\n".join(toml_lines)
|
||||||
|
|
||||||
@@ -308,29 +245,43 @@ class CommandRegistrar:
|
|||||||
if not isinstance(frontmatter, dict):
|
if not isinstance(frontmatter, dict):
|
||||||
frontmatter = {}
|
frontmatter = {}
|
||||||
|
|
||||||
if agent_name == "codex":
|
if agent_name in {"codex", "kimi"}:
|
||||||
body = self._resolve_codex_skill_placeholders(frontmatter, body, project_root)
|
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
|
||||||
|
|
||||||
description = frontmatter.get("description", f"Spec-kit workflow command: {skill_name}")
|
description = frontmatter.get("description", f"Spec-kit workflow command: {skill_name}")
|
||||||
|
skill_frontmatter = self.build_skill_frontmatter(
|
||||||
|
agent_name,
|
||||||
|
skill_name,
|
||||||
|
description,
|
||||||
|
f"{source_id}:{source_file}",
|
||||||
|
)
|
||||||
|
return self.render_frontmatter(skill_frontmatter) + "\n" + body
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_skill_frontmatter(
|
||||||
|
agent_name: str,
|
||||||
|
skill_name: str,
|
||||||
|
description: str,
|
||||||
|
source: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Build consistent SKILL.md frontmatter across all skill generators."""
|
||||||
skill_frontmatter = {
|
skill_frontmatter = {
|
||||||
"name": skill_name,
|
"name": skill_name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"compatibility": "Requires spec-kit project structure with .specify/ directory",
|
"compatibility": "Requires spec-kit project structure with .specify/ directory",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"author": "github-spec-kit",
|
"author": "github-spec-kit",
|
||||||
"source": f"{source_id}:{source_file}",
|
"source": source,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return self.render_frontmatter(skill_frontmatter) + "\n" + body
|
if agent_name == "claude":
|
||||||
|
# Claude skills should only run when explicitly invoked.
|
||||||
|
skill_frontmatter["disable-model-invocation"] = True
|
||||||
|
return skill_frontmatter
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_codex_skill_placeholders(frontmatter: dict, body: str, project_root: Path) -> str:
|
def resolve_skill_placeholders(agent_name: str, frontmatter: dict, body: str, project_root: Path) -> str:
|
||||||
"""Resolve script placeholders for Codex skill overrides.
|
"""Resolve script placeholders for skills-backed agents."""
|
||||||
|
|
||||||
This intentionally scopes the fix to Codex, which is the newly
|
|
||||||
migrated runtime path in this PR. Existing Kimi behavior is left
|
|
||||||
unchanged for now.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
from . import load_init_options
|
from . import load_init_options
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -346,7 +297,11 @@ class CommandRegistrar:
|
|||||||
if not isinstance(agent_scripts, dict):
|
if not isinstance(agent_scripts, dict):
|
||||||
agent_scripts = {}
|
agent_scripts = {}
|
||||||
|
|
||||||
script_variant = load_init_options(project_root).get("script")
|
init_opts = load_init_options(project_root)
|
||||||
|
if not isinstance(init_opts, dict):
|
||||||
|
init_opts = {}
|
||||||
|
|
||||||
|
script_variant = init_opts.get("script")
|
||||||
if script_variant not in {"sh", "ps"}:
|
if script_variant not in {"sh", "ps"}:
|
||||||
fallback_order = []
|
fallback_order = []
|
||||||
default_variant = "ps" if platform.system().lower().startswith("win") else "sh"
|
default_variant = "ps" if platform.system().lower().startswith("win") else "sh"
|
||||||
@@ -376,7 +331,8 @@ class CommandRegistrar:
|
|||||||
agent_script_command = agent_script_command.replace("{ARGS}", "$ARGUMENTS")
|
agent_script_command = agent_script_command.replace("{ARGS}", "$ARGUMENTS")
|
||||||
body = body.replace("{AGENT_SCRIPT}", agent_script_command)
|
body = body.replace("{AGENT_SCRIPT}", agent_script_command)
|
||||||
|
|
||||||
return body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", "codex")
|
body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name)
|
||||||
|
return CommandRegistrar.rewrite_project_relative_paths(body)
|
||||||
|
|
||||||
def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str:
|
def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str:
|
||||||
"""Convert argument placeholder format.
|
"""Convert argument placeholder format.
|
||||||
@@ -400,8 +356,9 @@ class CommandRegistrar:
|
|||||||
short_name = cmd_name
|
short_name = cmd_name
|
||||||
if short_name.startswith("speckit."):
|
if short_name.startswith("speckit."):
|
||||||
short_name = short_name[len("speckit."):]
|
short_name = short_name[len("speckit."):]
|
||||||
|
short_name = short_name.replace(".", "-")
|
||||||
|
|
||||||
return f"speckit.{short_name}" if agent_name == "kimi" else f"speckit-{short_name}"
|
return f"speckit-{short_name}"
|
||||||
|
|
||||||
def register_commands(
|
def register_commands(
|
||||||
self,
|
self,
|
||||||
@@ -428,6 +385,7 @@ class CommandRegistrar:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If agent is not supported
|
ValueError: If agent is not supported
|
||||||
"""
|
"""
|
||||||
|
self._ensure_configs()
|
||||||
if agent_name not in self.AGENT_CONFIGS:
|
if agent_name not in self.AGENT_CONFIGS:
|
||||||
raise ValueError(f"Unsupported agent: {agent_name}")
|
raise ValueError(f"Unsupported agent: {agent_name}")
|
||||||
|
|
||||||
@@ -527,6 +485,7 @@ class CommandRegistrar:
|
|||||||
"""
|
"""
|
||||||
results = {}
|
results = {}
|
||||||
|
|
||||||
|
self._ensure_configs()
|
||||||
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
||||||
agent_dir = project_root / agent_config["dir"]
|
agent_dir = project_root / agent_config["dir"]
|
||||||
|
|
||||||
@@ -554,6 +513,7 @@ class CommandRegistrar:
|
|||||||
registered_commands: Dict mapping agent names to command name lists
|
registered_commands: Dict mapping agent names to command name lists
|
||||||
project_root: Path to project root
|
project_root: Path to project root
|
||||||
"""
|
"""
|
||||||
|
self._ensure_configs()
|
||||||
for agent_name, cmd_names in registered_commands.items():
|
for agent_name, cmd_names in registered_commands.items():
|
||||||
if agent_name not in self.AGENT_CONFIGS:
|
if agent_name not in self.AGENT_CONFIGS:
|
||||||
continue
|
continue
|
||||||
@@ -571,3 +531,13 @@ class CommandRegistrar:
|
|||||||
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
||||||
if prompt_file.exists():
|
if prompt_file.exists():
|
||||||
prompt_file.unlink()
|
prompt_file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
# Populate AGENT_CONFIGS after class definition.
|
||||||
|
# Catches ImportError from circular imports during module loading;
|
||||||
|
# _configs_loaded stays False so the next explicit access retries.
|
||||||
|
try:
|
||||||
|
CommandRegistrar._ensure_configs()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,49 @@ import yaml
|
|||||||
from packaging import version as pkg_version
|
from packaging import version as pkg_version
|
||||||
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
from packaging.specifiers import SpecifierSet, InvalidSpecifier
|
||||||
|
|
||||||
|
_FALLBACK_CORE_COMMAND_NAMES = frozenset({
|
||||||
|
"analyze",
|
||||||
|
"checklist",
|
||||||
|
"clarify",
|
||||||
|
"constitution",
|
||||||
|
"implement",
|
||||||
|
"plan",
|
||||||
|
"specify",
|
||||||
|
"tasks",
|
||||||
|
"taskstoissues",
|
||||||
|
})
|
||||||
|
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_core_command_names() -> frozenset[str]:
|
||||||
|
"""Discover bundled core command names from the packaged templates.
|
||||||
|
|
||||||
|
Prefer the wheel-time ``core_pack`` bundle when present, and fall back to
|
||||||
|
the source checkout when running from the repository. If neither is
|
||||||
|
available, use the baked-in fallback set so validation still works.
|
||||||
|
"""
|
||||||
|
candidate_dirs = [
|
||||||
|
Path(__file__).parent / "core_pack" / "commands",
|
||||||
|
Path(__file__).resolve().parent.parent.parent / "templates" / "commands",
|
||||||
|
]
|
||||||
|
|
||||||
|
for commands_dir in candidate_dirs:
|
||||||
|
if not commands_dir.is_dir():
|
||||||
|
continue
|
||||||
|
|
||||||
|
command_names = {
|
||||||
|
command_file.stem
|
||||||
|
for command_file in commands_dir.iterdir()
|
||||||
|
if command_file.is_file() and command_file.suffix == ".md"
|
||||||
|
}
|
||||||
|
if command_names:
|
||||||
|
return frozenset(command_names)
|
||||||
|
|
||||||
|
return _FALLBACK_CORE_COMMAND_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
CORE_COMMAND_NAMES = _load_core_command_names()
|
||||||
|
|
||||||
|
|
||||||
class ExtensionError(Exception):
|
class ExtensionError(Exception):
|
||||||
"""Base exception for extension-related errors."""
|
"""Base exception for extension-related errors."""
|
||||||
@@ -149,7 +192,7 @@ class ExtensionManifest:
|
|||||||
raise ValidationError("Command missing 'name' or 'file'")
|
raise ValidationError("Command missing 'name' or 'file'")
|
||||||
|
|
||||||
# Validate command name format
|
# Validate command name format
|
||||||
if not re.match(r'^speckit\.[a-z0-9-]+\.[a-z0-9-]+$', cmd["name"]):
|
if EXTENSION_COMMAND_NAME_PATTERN.match(cmd["name"]) is None:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid command name '{cmd['name']}': "
|
f"Invalid command name '{cmd['name']}': "
|
||||||
"must follow pattern 'speckit.{extension}.{command}'"
|
"must follow pattern 'speckit.{extension}.{command}'"
|
||||||
@@ -446,6 +489,126 @@ class ExtensionManager:
|
|||||||
self.extensions_dir = project_root / ".specify" / "extensions"
|
self.extensions_dir = project_root / ".specify" / "extensions"
|
||||||
self.registry = ExtensionRegistry(self.extensions_dir)
|
self.registry = ExtensionRegistry(self.extensions_dir)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, str]:
|
||||||
|
"""Collect command and alias names declared by a manifest.
|
||||||
|
|
||||||
|
Performs install-time validation for extension-specific constraints:
|
||||||
|
- commands and aliases must use the canonical `speckit.{extension}.{command}` shape
|
||||||
|
- commands and aliases must use this extension's namespace
|
||||||
|
- command namespaces must not shadow core commands
|
||||||
|
- duplicate command/alias names inside one manifest are rejected
|
||||||
|
|
||||||
|
Args:
|
||||||
|
manifest: Parsed extension manifest
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Mapping of declared command/alias name -> kind ("command"/"alias")
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If any declared name is invalid
|
||||||
|
"""
|
||||||
|
if manifest.id in CORE_COMMAND_NAMES:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Extension ID '{manifest.id}' conflicts with core command namespace '{manifest.id}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
declared_names: Dict[str, str] = {}
|
||||||
|
|
||||||
|
for cmd in manifest.commands:
|
||||||
|
primary_name = cmd["name"]
|
||||||
|
aliases = cmd.get("aliases", [])
|
||||||
|
|
||||||
|
if aliases is None:
|
||||||
|
aliases = []
|
||||||
|
if not isinstance(aliases, list):
|
||||||
|
raise ValidationError(
|
||||||
|
f"Aliases for command '{primary_name}' must be a list"
|
||||||
|
)
|
||||||
|
|
||||||
|
for kind, name in [("command", primary_name)] + [
|
||||||
|
("alias", alias) for alias in aliases
|
||||||
|
]:
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValidationError(
|
||||||
|
f"{kind.capitalize()} for command '{primary_name}' must be a string"
|
||||||
|
)
|
||||||
|
|
||||||
|
match = EXTENSION_COMMAND_NAME_PATTERN.match(name)
|
||||||
|
if match is None:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Invalid {kind} '{name}': "
|
||||||
|
"must follow pattern 'speckit.{extension}.{command}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
namespace = match.group(1)
|
||||||
|
if namespace != manifest.id:
|
||||||
|
raise ValidationError(
|
||||||
|
f"{kind.capitalize()} '{name}' must use extension namespace '{manifest.id}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if namespace in CORE_COMMAND_NAMES:
|
||||||
|
raise ValidationError(
|
||||||
|
f"{kind.capitalize()} '{name}' conflicts with core command namespace '{namespace}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if name in declared_names:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Duplicate command or alias '{name}' in extension manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
declared_names[name] = kind
|
||||||
|
|
||||||
|
return declared_names
|
||||||
|
|
||||||
|
def _get_installed_command_name_map(
|
||||||
|
self,
|
||||||
|
exclude_extension_id: Optional[str] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""Return registered command and alias names for installed extensions."""
|
||||||
|
installed_names: Dict[str, str] = {}
|
||||||
|
|
||||||
|
for ext_id in self.registry.keys():
|
||||||
|
if ext_id == exclude_extension_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
manifest = self.get_extension(ext_id)
|
||||||
|
if manifest is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for cmd in manifest.commands:
|
||||||
|
cmd_name = cmd.get("name")
|
||||||
|
if isinstance(cmd_name, str):
|
||||||
|
installed_names.setdefault(cmd_name, ext_id)
|
||||||
|
|
||||||
|
aliases = cmd.get("aliases", [])
|
||||||
|
if not isinstance(aliases, list):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for alias in aliases:
|
||||||
|
if isinstance(alias, str):
|
||||||
|
installed_names.setdefault(alias, ext_id)
|
||||||
|
|
||||||
|
return installed_names
|
||||||
|
|
||||||
|
def _validate_install_conflicts(self, manifest: ExtensionManifest) -> None:
|
||||||
|
"""Reject installs that would shadow core or installed extension commands."""
|
||||||
|
declared_names = self._collect_manifest_command_names(manifest)
|
||||||
|
installed_names = self._get_installed_command_name_map(
|
||||||
|
exclude_extension_id=manifest.id
|
||||||
|
)
|
||||||
|
|
||||||
|
collisions = [
|
||||||
|
f"{name} (already provided by extension '{installed_names[name]}')"
|
||||||
|
for name in sorted(declared_names)
|
||||||
|
if name in installed_names
|
||||||
|
]
|
||||||
|
if collisions:
|
||||||
|
raise ValidationError(
|
||||||
|
"Extension commands conflict with installed extensions:\n- "
|
||||||
|
+ "\n- ".join(collisions)
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_extensionignore(source_dir: Path) -> Optional[Callable[[str, List[str]], Set[str]]]:
|
def _load_extensionignore(source_dir: Path) -> Optional[Callable[[str, List[str]], Set[str]]]:
|
||||||
"""Load .extensionignore and return an ignore function for shutil.copytree.
|
"""Load .extensionignore and return an ignore function for shutil.copytree.
|
||||||
@@ -511,24 +674,32 @@ class ExtensionManager:
|
|||||||
return _ignore
|
return _ignore
|
||||||
|
|
||||||
def _get_skills_dir(self) -> Optional[Path]:
|
def _get_skills_dir(self) -> Optional[Path]:
|
||||||
"""Return the skills directory if ``--ai-skills`` was used during init.
|
"""Return the active skills directory for extension skill registration.
|
||||||
|
|
||||||
Reads ``.specify/init-options.json`` to determine whether skills
|
Reads ``.specify/init-options.json`` to determine whether skills
|
||||||
are enabled and which agent was selected, then delegates to
|
are enabled and which agent was selected, then delegates to
|
||||||
the module-level ``_get_skills_dir()`` helper for the concrete path.
|
the module-level ``_get_skills_dir()`` helper for the concrete path.
|
||||||
|
|
||||||
|
Kimi is treated as a native-skills agent: if ``ai == "kimi"`` and
|
||||||
|
``.kimi/skills`` exists, extension installs should still propagate
|
||||||
|
command skills even when ``ai_skills`` is false.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The skills directory ``Path``, or ``None`` if skills were not
|
The skills directory ``Path``, or ``None`` if skills were not
|
||||||
enabled or the init-options file is missing.
|
enabled and no native-skills fallback applies.
|
||||||
"""
|
"""
|
||||||
from . import load_init_options, _get_skills_dir as resolve_skills_dir
|
from . import load_init_options, _get_skills_dir as resolve_skills_dir
|
||||||
|
|
||||||
opts = load_init_options(self.project_root)
|
opts = load_init_options(self.project_root)
|
||||||
if not opts.get("ai_skills"):
|
if not isinstance(opts, dict):
|
||||||
return None
|
opts = {}
|
||||||
|
|
||||||
agent = opts.get("ai")
|
agent = opts.get("ai")
|
||||||
if not agent:
|
if not isinstance(agent, str) or not agent:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ai_skills_enabled = bool(opts.get("ai_skills"))
|
||||||
|
if not ai_skills_enabled and agent != "kimi":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
skills_dir = resolve_skills_dir(self.project_root, agent)
|
skills_dir = resolve_skills_dir(self.project_root, agent)
|
||||||
@@ -561,12 +732,17 @@ class ExtensionManager:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
from . import load_init_options
|
from . import load_init_options
|
||||||
|
from .agents import CommandRegistrar
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
opts = load_init_options(self.project_root)
|
|
||||||
selected_ai = opts.get("ai", "")
|
|
||||||
|
|
||||||
written: List[str] = []
|
written: List[str] = []
|
||||||
|
opts = load_init_options(self.project_root)
|
||||||
|
if not isinstance(opts, dict):
|
||||||
|
opts = {}
|
||||||
|
selected_ai = opts.get("ai")
|
||||||
|
if not isinstance(selected_ai, str) or not selected_ai:
|
||||||
|
return []
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
|
||||||
for cmd_info in manifest.commands:
|
for cmd_info in manifest.commands:
|
||||||
cmd_name = cmd_info["name"]
|
cmd_name = cmd_info["name"]
|
||||||
@@ -587,17 +763,12 @@ class ExtensionManager:
|
|||||||
if not source_file.is_file():
|
if not source_file.is_file():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Derive skill name from command name, matching the convention used by
|
# Derive skill name from command name using the same hyphenated
|
||||||
# presets.py: strip the leading "speckit." prefix, then form:
|
# convention as hook rendering and preset skill registration.
|
||||||
# Kimi → "speckit.{short_name}" (dot preserved for Kimi agent)
|
|
||||||
# other → "speckit-{short_name}" (hyphen separator)
|
|
||||||
short_name_raw = cmd_name
|
short_name_raw = cmd_name
|
||||||
if short_name_raw.startswith("speckit."):
|
if short_name_raw.startswith("speckit."):
|
||||||
short_name_raw = short_name_raw[len("speckit."):]
|
short_name_raw = short_name_raw[len("speckit."):]
|
||||||
if selected_ai == "kimi":
|
skill_name = f"speckit-{short_name_raw.replace('.', '-')}"
|
||||||
skill_name = f"speckit.{short_name_raw}"
|
|
||||||
else:
|
|
||||||
skill_name = f"speckit-{short_name_raw}"
|
|
||||||
|
|
||||||
# Check if skill already exists before creating the directory
|
# Check if skill already exists before creating the directory
|
||||||
skill_subdir = skills_dir / skill_name
|
skill_subdir = skills_dir / skill_name
|
||||||
@@ -621,35 +792,21 @@ class ExtensionManager:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass # best-effort cleanup
|
pass # best-effort cleanup
|
||||||
continue
|
continue
|
||||||
if content.startswith("---"):
|
frontmatter, body = registrar.parse_frontmatter(content)
|
||||||
parts = content.split("---", 2)
|
frontmatter = registrar._adjust_script_paths(frontmatter)
|
||||||
if len(parts) >= 3:
|
body = registrar.resolve_skill_placeholders(
|
||||||
try:
|
selected_ai, frontmatter, body, self.project_root
|
||||||
frontmatter = yaml.safe_load(parts[1])
|
)
|
||||||
except yaml.YAMLError:
|
|
||||||
frontmatter = {}
|
|
||||||
if not isinstance(frontmatter, dict):
|
|
||||||
frontmatter = {}
|
|
||||||
body = parts[2].strip()
|
|
||||||
else:
|
|
||||||
frontmatter = {}
|
|
||||||
body = content
|
|
||||||
else:
|
|
||||||
frontmatter = {}
|
|
||||||
body = content
|
|
||||||
|
|
||||||
original_desc = frontmatter.get("description", "")
|
original_desc = frontmatter.get("description", "")
|
||||||
description = original_desc or f"Extension command: {cmd_name}"
|
description = original_desc or f"Extension command: {cmd_name}"
|
||||||
|
|
||||||
frontmatter_data = {
|
frontmatter_data = registrar.build_skill_frontmatter(
|
||||||
"name": skill_name,
|
selected_ai,
|
||||||
"description": description,
|
skill_name,
|
||||||
"compatibility": "Requires spec-kit project structure with .specify/ directory",
|
description,
|
||||||
"metadata": {
|
f"extension:{manifest.id}",
|
||||||
"author": "github-spec-kit",
|
)
|
||||||
"source": f"extension:{manifest.id}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
|
frontmatter_text = yaml.safe_dump(frontmatter_data, sort_keys=False).strip()
|
||||||
|
|
||||||
# Derive a human-friendly title from the command name
|
# Derive a human-friendly title from the command name
|
||||||
@@ -738,11 +895,9 @@ class ExtensionManager:
|
|||||||
shutil.rmtree(skill_subdir)
|
shutil.rmtree(skill_subdir)
|
||||||
else:
|
else:
|
||||||
# Fallback: scan all possible agent skills directories
|
# Fallback: scan all possible agent skills directories
|
||||||
from . import AGENT_CONFIG, AGENT_SKILLS_DIR_OVERRIDES, DEFAULT_SKILLS_DIR
|
from . import AGENT_CONFIG, DEFAULT_SKILLS_DIR
|
||||||
|
|
||||||
candidate_dirs: set[Path] = set()
|
candidate_dirs: set[Path] = set()
|
||||||
for override_path in AGENT_SKILLS_DIR_OVERRIDES.values():
|
|
||||||
candidate_dirs.add(self.project_root / override_path)
|
|
||||||
for cfg in AGENT_CONFIG.values():
|
for cfg in AGENT_CONFIG.values():
|
||||||
folder = cfg.get("folder", "")
|
folder = cfg.get("folder", "")
|
||||||
if folder:
|
if folder:
|
||||||
@@ -866,6 +1021,9 @@ class ExtensionManager:
|
|||||||
f"Use 'specify extension remove {manifest.id}' first."
|
f"Use 'specify extension remove {manifest.id}' first."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Reject manifests that would shadow core commands or installed extensions.
|
||||||
|
self._validate_install_conflicts(manifest)
|
||||||
|
|
||||||
# Install extension
|
# Install extension
|
||||||
dest_dir = self.extensions_dir / manifest.id
|
dest_dir = self.extensions_dir / manifest.id
|
||||||
if dest_dir.exists():
|
if dest_dir.exists():
|
||||||
@@ -1940,6 +2098,55 @@ class HookExecutor:
|
|||||||
self.project_root = project_root
|
self.project_root = project_root
|
||||||
self.extensions_dir = project_root / ".specify" / "extensions"
|
self.extensions_dir = project_root / ".specify" / "extensions"
|
||||||
self.config_file = project_root / ".specify" / "extensions.yml"
|
self.config_file = project_root / ".specify" / "extensions.yml"
|
||||||
|
self._init_options_cache: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
def _load_init_options(self) -> Dict[str, Any]:
|
||||||
|
"""Load persisted init options used to determine invocation style.
|
||||||
|
|
||||||
|
Uses the shared helper from specify_cli and caches values per executor
|
||||||
|
instance to avoid repeated filesystem reads during hook rendering.
|
||||||
|
"""
|
||||||
|
if self._init_options_cache is None:
|
||||||
|
from . import load_init_options
|
||||||
|
|
||||||
|
payload = load_init_options(self.project_root)
|
||||||
|
self._init_options_cache = payload if isinstance(payload, dict) else {}
|
||||||
|
return self._init_options_cache
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _skill_name_from_command(command: Any) -> str:
|
||||||
|
"""Map a command id like speckit.plan to speckit-plan skill name."""
|
||||||
|
if not isinstance(command, str):
|
||||||
|
return ""
|
||||||
|
command_id = command.strip()
|
||||||
|
if not command_id.startswith("speckit."):
|
||||||
|
return ""
|
||||||
|
return f"speckit-{command_id[len('speckit.'):].replace('.', '-')}"
|
||||||
|
|
||||||
|
def _render_hook_invocation(self, command: Any) -> str:
|
||||||
|
"""Render an agent-specific invocation string for a hook command."""
|
||||||
|
if not isinstance(command, str):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
command_id = command.strip()
|
||||||
|
if not command_id:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
init_options = self._load_init_options()
|
||||||
|
selected_ai = init_options.get("ai")
|
||||||
|
codex_skill_mode = selected_ai == "codex" and bool(init_options.get("ai_skills"))
|
||||||
|
claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills"))
|
||||||
|
kimi_skill_mode = selected_ai == "kimi"
|
||||||
|
|
||||||
|
skill_name = self._skill_name_from_command(command_id)
|
||||||
|
if codex_skill_mode and skill_name:
|
||||||
|
return f"${skill_name}"
|
||||||
|
if claude_skill_mode and skill_name:
|
||||||
|
return f"/{skill_name}"
|
||||||
|
if kimi_skill_mode and skill_name:
|
||||||
|
return f"/skill:{skill_name}"
|
||||||
|
|
||||||
|
return f"/{command_id}"
|
||||||
|
|
||||||
def get_project_config(self) -> Dict[str, Any]:
|
def get_project_config(self) -> Dict[str, Any]:
|
||||||
"""Load project-level extension configuration.
|
"""Load project-level extension configuration.
|
||||||
@@ -2183,21 +2390,27 @@ class HookExecutor:
|
|||||||
for hook in hooks:
|
for hook in hooks:
|
||||||
extension = hook.get("extension")
|
extension = hook.get("extension")
|
||||||
command = hook.get("command")
|
command = hook.get("command")
|
||||||
|
invocation = self._render_hook_invocation(command)
|
||||||
|
command_text = command if isinstance(command, str) and command.strip() else "<missing command>"
|
||||||
|
display_invocation = invocation or (
|
||||||
|
f"/{command_text}" if command_text != "<missing command>" else "/<missing command>"
|
||||||
|
)
|
||||||
optional = hook.get("optional", True)
|
optional = hook.get("optional", True)
|
||||||
prompt = hook.get("prompt", "")
|
prompt = hook.get("prompt", "")
|
||||||
description = hook.get("description", "")
|
description = hook.get("description", "")
|
||||||
|
|
||||||
if optional:
|
if optional:
|
||||||
lines.append(f"\n**Optional Hook**: {extension}")
|
lines.append(f"\n**Optional Hook**: {extension}")
|
||||||
lines.append(f"Command: `/{command}`")
|
lines.append(f"Command: `{display_invocation}`")
|
||||||
if description:
|
if description:
|
||||||
lines.append(f"Description: {description}")
|
lines.append(f"Description: {description}")
|
||||||
lines.append(f"\nPrompt: {prompt}")
|
lines.append(f"\nPrompt: {prompt}")
|
||||||
lines.append(f"To execute: `/{command}`")
|
lines.append(f"To execute: `{display_invocation}`")
|
||||||
else:
|
else:
|
||||||
lines.append(f"\n**Automatic Hook**: {extension}")
|
lines.append(f"\n**Automatic Hook**: {extension}")
|
||||||
lines.append(f"Executing: `/{command}`")
|
lines.append(f"Executing: `{display_invocation}`")
|
||||||
lines.append(f"EXECUTE_COMMAND: {command}")
|
lines.append(f"EXECUTE_COMMAND: {command_text}")
|
||||||
|
lines.append(f"EXECUTE_COMMAND_INVOCATION: {display_invocation}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -2261,6 +2474,7 @@ class HookExecutor:
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"command": hook.get("command"),
|
"command": hook.get("command"),
|
||||||
|
"invocation": self._render_hook_invocation(hook.get("command")),
|
||||||
"extension": hook.get("extension"),
|
"extension": hook.get("extension"),
|
||||||
"optional": hook.get("optional", True),
|
"optional": hook.get("optional", True),
|
||||||
"description": hook.get("description", ""),
|
"description": hook.get("description", ""),
|
||||||
@@ -2304,4 +2518,3 @@ class HookExecutor:
|
|||||||
hook["enabled"] = False
|
hook["enabled"] = False
|
||||||
|
|
||||||
self.save_project_config(config)
|
self.save_project_config(config)
|
||||||
|
|
||||||
|
|||||||
105
src/specify_cli/integrations/__init__.py
Normal file
105
src/specify_cli/integrations/__init__.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
"""Integration registry for AI coding assistants.
|
||||||
|
|
||||||
|
Each integration is a self-contained subpackage that handles setup/teardown
|
||||||
|
for a specific AI assistant (Copilot, Claude, Gemini, etc.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .base import IntegrationBase
|
||||||
|
|
||||||
|
# Maps integration key → IntegrationBase instance.
|
||||||
|
# Populated by later stages as integrations are migrated.
|
||||||
|
INTEGRATION_REGISTRY: dict[str, IntegrationBase] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _register(integration: IntegrationBase) -> None:
|
||||||
|
"""Register an integration instance in the global registry.
|
||||||
|
|
||||||
|
Raises ``ValueError`` for falsy keys and ``KeyError`` for duplicates.
|
||||||
|
"""
|
||||||
|
key = integration.key
|
||||||
|
if not key:
|
||||||
|
raise ValueError("Cannot register integration with an empty key.")
|
||||||
|
if key in INTEGRATION_REGISTRY:
|
||||||
|
raise KeyError(f"Integration with key {key!r} is already registered.")
|
||||||
|
INTEGRATION_REGISTRY[key] = integration
|
||||||
|
|
||||||
|
|
||||||
|
def get_integration(key: str) -> IntegrationBase | None:
|
||||||
|
"""Return the integration for *key*, or ``None`` if not registered."""
|
||||||
|
return INTEGRATION_REGISTRY.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Register built-in integrations --------------------------------------
|
||||||
|
|
||||||
|
def _register_builtins() -> None:
|
||||||
|
"""Register all built-in integrations.
|
||||||
|
|
||||||
|
Package directories use Python-safe identifiers (e.g. ``kiro_cli``,
|
||||||
|
``cursor_agent``). The user-facing integration key stored in
|
||||||
|
``IntegrationBase.key`` stays hyphenated (``"kiro-cli"``,
|
||||||
|
``"cursor-agent"``) to match the actual CLI tool / binary name that
|
||||||
|
users install and invoke.
|
||||||
|
"""
|
||||||
|
# -- Imports (alphabetical) -------------------------------------------
|
||||||
|
from .agy import AgyIntegration
|
||||||
|
from .amp import AmpIntegration
|
||||||
|
from .auggie import AuggieIntegration
|
||||||
|
from .bob import BobIntegration
|
||||||
|
from .claude import ClaudeIntegration
|
||||||
|
from .codex import CodexIntegration
|
||||||
|
from .codebuddy import CodebuddyIntegration
|
||||||
|
from .copilot import CopilotIntegration
|
||||||
|
from .cursor_agent import CursorAgentIntegration
|
||||||
|
from .gemini import GeminiIntegration
|
||||||
|
from .generic import GenericIntegration
|
||||||
|
from .iflow import IflowIntegration
|
||||||
|
from .junie import JunieIntegration
|
||||||
|
from .kilocode import KilocodeIntegration
|
||||||
|
from .kimi import KimiIntegration
|
||||||
|
from .kiro_cli import KiroCliIntegration
|
||||||
|
from .opencode import OpencodeIntegration
|
||||||
|
from .pi import PiIntegration
|
||||||
|
from .qodercli import QodercliIntegration
|
||||||
|
from .qwen import QwenIntegration
|
||||||
|
from .roo import RooIntegration
|
||||||
|
from .shai import ShaiIntegration
|
||||||
|
from .tabnine import TabnineIntegration
|
||||||
|
from .trae import TraeIntegration
|
||||||
|
from .vibe import VibeIntegration
|
||||||
|
from .windsurf import WindsurfIntegration
|
||||||
|
|
||||||
|
# -- Registration (alphabetical) --------------------------------------
|
||||||
|
_register(AgyIntegration())
|
||||||
|
_register(AmpIntegration())
|
||||||
|
_register(AuggieIntegration())
|
||||||
|
_register(BobIntegration())
|
||||||
|
_register(ClaudeIntegration())
|
||||||
|
_register(CodexIntegration())
|
||||||
|
_register(CodebuddyIntegration())
|
||||||
|
_register(CopilotIntegration())
|
||||||
|
_register(CursorAgentIntegration())
|
||||||
|
_register(GeminiIntegration())
|
||||||
|
_register(GenericIntegration())
|
||||||
|
_register(IflowIntegration())
|
||||||
|
_register(JunieIntegration())
|
||||||
|
_register(KilocodeIntegration())
|
||||||
|
_register(KimiIntegration())
|
||||||
|
_register(KiroCliIntegration())
|
||||||
|
_register(OpencodeIntegration())
|
||||||
|
_register(PiIntegration())
|
||||||
|
_register(QodercliIntegration())
|
||||||
|
_register(QwenIntegration())
|
||||||
|
_register(RooIntegration())
|
||||||
|
_register(ShaiIntegration())
|
||||||
|
_register(TabnineIntegration())
|
||||||
|
_register(TraeIntegration())
|
||||||
|
_register(VibeIntegration())
|
||||||
|
_register(WindsurfIntegration())
|
||||||
|
|
||||||
|
|
||||||
|
_register_builtins()
|
||||||
41
src/specify_cli/integrations/agy/__init__.py
Normal file
41
src/specify_cli/integrations/agy/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""Antigravity (agy) integration — skills-based agent.
|
||||||
|
|
||||||
|
Antigravity uses ``.agent/skills/speckit-<name>/SKILL.md`` layout.
|
||||||
|
Explicit command support was deprecated in version 1.20.5;
|
||||||
|
``--skills`` defaults to ``True``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..base import IntegrationOption, SkillsIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class AgyIntegration(SkillsIntegration):
|
||||||
|
"""Integration for Antigravity IDE."""
|
||||||
|
|
||||||
|
key = "agy"
|
||||||
|
config = {
|
||||||
|
"name": "Antigravity",
|
||||||
|
"folder": ".agent/",
|
||||||
|
"commands_subdir": "skills",
|
||||||
|
"install_url": None,
|
||||||
|
"requires_cli": False,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".agent/skills",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": "/SKILL.md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def options(cls) -> list[IntegrationOption]:
|
||||||
|
return [
|
||||||
|
IntegrationOption(
|
||||||
|
"--skills",
|
||||||
|
is_flag=True,
|
||||||
|
default=True,
|
||||||
|
help="Install as agent skills (default for Antigravity since v1.20.5)",
|
||||||
|
),
|
||||||
|
]
|
||||||
17
src/specify_cli/integrations/agy/scripts/update-context.ps1
Normal file
17
src/specify_cli/integrations/agy/scripts/update-context.ps1
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# update-context.ps1 — Antigravity (agy) integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType agy
|
||||||
24
src/specify_cli/integrations/agy/scripts/update-context.sh
Executable file
24
src/specify_cli/integrations/agy/scripts/update-context.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Antigravity (agy) integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" agy
|
||||||
21
src/specify_cli/integrations/amp/__init__.py
Normal file
21
src/specify_cli/integrations/amp/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Amp CLI integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class AmpIntegration(MarkdownIntegration):
|
||||||
|
key = "amp"
|
||||||
|
config = {
|
||||||
|
"name": "Amp",
|
||||||
|
"folder": ".agents/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://ampcode.com/manual#install",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".agents/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
23
src/specify_cli/integrations/amp/scripts/update-context.ps1
Normal file
23
src/specify_cli/integrations/amp/scripts/update-context.ps1
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Amp integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp
|
||||||
28
src/specify_cli/integrations/amp/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/amp/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Amp integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp
|
||||||
21
src/specify_cli/integrations/auggie/__init__.py
Normal file
21
src/specify_cli/integrations/auggie/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Auggie CLI integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class AuggieIntegration(MarkdownIntegration):
|
||||||
|
key = "auggie"
|
||||||
|
config = {
|
||||||
|
"name": "Auggie CLI",
|
||||||
|
"folder": ".augment/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".augment/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = ".augment/rules/specify-rules.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie
|
||||||
28
src/specify_cli/integrations/auggie/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/auggie/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie
|
||||||
793
src/specify_cli/integrations/base.py
Normal file
793
src/specify_cli/integrations/base.py
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
"""Base classes for AI-assistant integrations.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- ``IntegrationOption`` — declares a CLI option an integration accepts.
|
||||||
|
- ``IntegrationBase`` — abstract base every integration must implement.
|
||||||
|
- ``MarkdownIntegration`` — concrete base for standard Markdown-format
|
||||||
|
integrations (the common case — subclass, set three class attrs, done).
|
||||||
|
- ``TomlIntegration`` — concrete base for TOML-format integrations
|
||||||
|
(Gemini, Tabnine — subclass, set three class attrs, done).
|
||||||
|
- ``SkillsIntegration`` — concrete base for integrations that install
|
||||||
|
commands as agent skills (``speckit-<name>/SKILL.md`` layout).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from abc import ABC
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .manifest import IntegrationManifest
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IntegrationOption
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IntegrationOption:
|
||||||
|
"""Declares an option that an integration accepts via ``--integration-options``.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: The flag name (e.g. ``"--commands-dir"``).
|
||||||
|
is_flag: ``True`` for boolean flags (``--skills``).
|
||||||
|
required: ``True`` if the option must be supplied.
|
||||||
|
default: Default value when not supplied (``None`` → no default).
|
||||||
|
help: One-line description shown in ``specify integrate info``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
is_flag: bool = False
|
||||||
|
required: bool = False
|
||||||
|
default: Any = None
|
||||||
|
help: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IntegrationBase — abstract base class
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class IntegrationBase(ABC):
|
||||||
|
"""Abstract base class every integration must implement.
|
||||||
|
|
||||||
|
Subclasses must set the following class-level attributes:
|
||||||
|
|
||||||
|
* ``key`` — unique identifier, matches actual CLI tool name
|
||||||
|
* ``config`` — dict compatible with ``AGENT_CONFIG`` entries
|
||||||
|
* ``registrar_config`` — dict compatible with ``CommandRegistrar.AGENT_CONFIGS``
|
||||||
|
|
||||||
|
And may optionally set:
|
||||||
|
|
||||||
|
* ``context_file`` — path (relative to project root) of the agent
|
||||||
|
context/instructions file (e.g. ``"CLAUDE.md"``)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Must be set by every subclass ------------------------------------
|
||||||
|
|
||||||
|
key: str = ""
|
||||||
|
"""Unique integration key — should match the actual CLI tool name."""
|
||||||
|
|
||||||
|
config: dict[str, Any] | None = None
|
||||||
|
"""Metadata dict matching the ``AGENT_CONFIG`` shape."""
|
||||||
|
|
||||||
|
registrar_config: dict[str, Any] | None = None
|
||||||
|
"""Registration dict matching ``CommandRegistrar.AGENT_CONFIGS`` shape."""
|
||||||
|
|
||||||
|
# -- Optional ---------------------------------------------------------
|
||||||
|
|
||||||
|
context_file: str | None = None
|
||||||
|
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
|
||||||
|
|
||||||
|
# -- Public API -------------------------------------------------------
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def options(cls) -> list[IntegrationOption]:
|
||||||
|
"""Return options this integration accepts. Default: none."""
|
||||||
|
return []
|
||||||
|
|
||||||
|
# -- Primitives — building blocks for setup() -------------------------
|
||||||
|
|
||||||
|
def shared_commands_dir(self) -> Path | None:
|
||||||
|
"""Return path to the shared command templates directory.
|
||||||
|
|
||||||
|
Checks ``core_pack/commands/`` (wheel install) first, then
|
||||||
|
``templates/commands/`` (source checkout). Returns ``None``
|
||||||
|
if neither exists.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
pkg_dir = Path(inspect.getfile(IntegrationBase)).resolve().parent.parent
|
||||||
|
for candidate in [
|
||||||
|
pkg_dir / "core_pack" / "commands",
|
||||||
|
pkg_dir.parent.parent / "templates" / "commands",
|
||||||
|
]:
|
||||||
|
if candidate.is_dir():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
def shared_templates_dir(self) -> Path | None:
|
||||||
|
"""Return path to the shared page templates directory.
|
||||||
|
|
||||||
|
Contains ``vscode-settings.json``, ``spec-template.md``, etc.
|
||||||
|
Checks ``core_pack/templates/`` then ``templates/``.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
pkg_dir = Path(inspect.getfile(IntegrationBase)).resolve().parent.parent
|
||||||
|
for candidate in [
|
||||||
|
pkg_dir / "core_pack" / "templates",
|
||||||
|
pkg_dir.parent.parent / "templates",
|
||||||
|
]:
|
||||||
|
if candidate.is_dir():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_command_templates(self) -> list[Path]:
|
||||||
|
"""Return sorted list of command template files from the shared directory."""
|
||||||
|
cmd_dir = self.shared_commands_dir()
|
||||||
|
if not cmd_dir or not cmd_dir.is_dir():
|
||||||
|
return []
|
||||||
|
return sorted(f for f in cmd_dir.iterdir() if f.is_file() and f.suffix == ".md")
|
||||||
|
|
||||||
|
def command_filename(self, template_name: str) -> str:
|
||||||
|
"""Return the destination filename for a command template.
|
||||||
|
|
||||||
|
*template_name* is the stem of the source file (e.g. ``"plan"``).
|
||||||
|
Default: ``speckit.{template_name}.md``. Subclasses override
|
||||||
|
to change the extension or naming convention.
|
||||||
|
"""
|
||||||
|
return f"speckit.{template_name}.md"
|
||||||
|
|
||||||
|
def commands_dest(self, project_root: Path) -> Path:
|
||||||
|
"""Return the absolute path to the commands output directory.
|
||||||
|
|
||||||
|
Derived from ``config["folder"]`` and ``config["commands_subdir"]``.
|
||||||
|
Raises ``ValueError`` if ``config`` or ``folder`` is missing.
|
||||||
|
"""
|
||||||
|
if not self.config:
|
||||||
|
raise ValueError(
|
||||||
|
f"{type(self).__name__}.config is not set; integration "
|
||||||
|
"subclasses must define a non-empty 'config' mapping."
|
||||||
|
)
|
||||||
|
folder = self.config.get("folder")
|
||||||
|
if not folder:
|
||||||
|
raise ValueError(
|
||||||
|
f"{type(self).__name__}.config is missing required 'folder' entry."
|
||||||
|
)
|
||||||
|
subdir = self.config.get("commands_subdir", "commands")
|
||||||
|
return project_root / folder / subdir
|
||||||
|
|
||||||
|
# -- File operations — granular primitives for setup() ----------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_command_to_directory(
|
||||||
|
src: Path,
|
||||||
|
dest_dir: Path,
|
||||||
|
filename: str,
|
||||||
|
) -> Path:
|
||||||
|
"""Copy a command template to *dest_dir* with the given *filename*.
|
||||||
|
|
||||||
|
Creates *dest_dir* if needed. Returns the absolute path of the
|
||||||
|
written file. The caller can post-process the file before
|
||||||
|
recording it in the manifest.
|
||||||
|
"""
|
||||||
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
dst = dest_dir / filename
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
return dst
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def record_file_in_manifest(
|
||||||
|
file_path: Path,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
) -> None:
|
||||||
|
"""Hash *file_path* and record it in *manifest*.
|
||||||
|
|
||||||
|
*file_path* must be inside *project_root*.
|
||||||
|
"""
|
||||||
|
rel = file_path.resolve().relative_to(project_root.resolve())
|
||||||
|
manifest.record_existing(rel)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_file_and_record(
|
||||||
|
content: str,
|
||||||
|
dest: Path,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
) -> Path:
|
||||||
|
"""Write *content* to *dest*, hash it, and record in *manifest*.
|
||||||
|
|
||||||
|
Creates parent directories as needed. Writes bytes directly to
|
||||||
|
avoid platform newline translation (CRLF on Windows). Any
|
||||||
|
``\r\n`` sequences in *content* are normalised to ``\n`` before
|
||||||
|
writing. Returns *dest*.
|
||||||
|
"""
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
normalized = content.replace("\r\n", "\n")
|
||||||
|
dest.write_bytes(normalized.encode("utf-8"))
|
||||||
|
rel = dest.resolve().relative_to(project_root.resolve())
|
||||||
|
manifest.record_existing(rel)
|
||||||
|
return dest
|
||||||
|
|
||||||
|
def integration_scripts_dir(self) -> Path | None:
|
||||||
|
"""Return path to this integration's bundled ``scripts/`` directory.
|
||||||
|
|
||||||
|
Looks for a ``scripts/`` sibling of the module that defines the
|
||||||
|
concrete subclass (not ``IntegrationBase`` itself).
|
||||||
|
Returns ``None`` if the directory doesn't exist.
|
||||||
|
"""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
cls_file = inspect.getfile(type(self))
|
||||||
|
scripts = Path(cls_file).resolve().parent / "scripts"
|
||||||
|
return scripts if scripts.is_dir() else None
|
||||||
|
|
||||||
|
def install_scripts(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Copy integration-specific scripts into the project.
|
||||||
|
|
||||||
|
Copies files from this integration's ``scripts/`` directory to
|
||||||
|
``.specify/integrations/<key>/scripts/`` in the project. Shell
|
||||||
|
scripts are made executable. All copied files are recorded in
|
||||||
|
*manifest*.
|
||||||
|
|
||||||
|
Returns the list of files created.
|
||||||
|
"""
|
||||||
|
scripts_src = self.integration_scripts_dir()
|
||||||
|
if not scripts_src:
|
||||||
|
return []
|
||||||
|
|
||||||
|
created: list[Path] = []
|
||||||
|
scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts"
|
||||||
|
scripts_dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for src_script in sorted(scripts_src.iterdir()):
|
||||||
|
if not src_script.is_file():
|
||||||
|
continue
|
||||||
|
dst_script = scripts_dest / src_script.name
|
||||||
|
shutil.copy2(src_script, dst_script)
|
||||||
|
if dst_script.suffix == ".sh":
|
||||||
|
dst_script.chmod(dst_script.stat().st_mode | 0o111)
|
||||||
|
self.record_file_in_manifest(dst_script, project_root, manifest)
|
||||||
|
created.append(dst_script)
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def process_template(
|
||||||
|
content: str,
|
||||||
|
agent_name: str,
|
||||||
|
script_type: str,
|
||||||
|
arg_placeholder: str = "$ARGUMENTS",
|
||||||
|
) -> str:
|
||||||
|
"""Process a raw command template into agent-ready content.
|
||||||
|
|
||||||
|
Performs the same transformations as the release script:
|
||||||
|
1. Extract ``scripts.<script_type>`` value from YAML frontmatter
|
||||||
|
2. Replace ``{SCRIPT}`` with the extracted script command
|
||||||
|
3. Extract ``agent_scripts.<script_type>`` and replace ``{AGENT_SCRIPT}``
|
||||||
|
4. Strip ``scripts:`` and ``agent_scripts:`` sections from frontmatter
|
||||||
|
5. Replace ``{ARGS}`` with *arg_placeholder*
|
||||||
|
6. Replace ``__AGENT__`` with *agent_name*
|
||||||
|
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
||||||
|
"""
|
||||||
|
# 1. Extract script command from frontmatter
|
||||||
|
script_command = ""
|
||||||
|
script_pattern = re.compile(
|
||||||
|
rf"^\s*{re.escape(script_type)}:\s*(.+)$", re.MULTILINE
|
||||||
|
)
|
||||||
|
# Find the scripts: block
|
||||||
|
in_scripts = False
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.strip() == "scripts:":
|
||||||
|
in_scripts = True
|
||||||
|
continue
|
||||||
|
if in_scripts and line and not line[0].isspace():
|
||||||
|
in_scripts = False
|
||||||
|
if in_scripts:
|
||||||
|
m = script_pattern.match(line)
|
||||||
|
if m:
|
||||||
|
script_command = m.group(1).strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. Replace {SCRIPT}
|
||||||
|
if script_command:
|
||||||
|
content = content.replace("{SCRIPT}", script_command)
|
||||||
|
|
||||||
|
# 3. Extract agent_script command
|
||||||
|
agent_script_command = ""
|
||||||
|
in_agent_scripts = False
|
||||||
|
for line in content.splitlines():
|
||||||
|
if line.strip() == "agent_scripts:":
|
||||||
|
in_agent_scripts = True
|
||||||
|
continue
|
||||||
|
if in_agent_scripts and line and not line[0].isspace():
|
||||||
|
in_agent_scripts = False
|
||||||
|
if in_agent_scripts:
|
||||||
|
m = script_pattern.match(line)
|
||||||
|
if m:
|
||||||
|
agent_script_command = m.group(1).strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
if agent_script_command:
|
||||||
|
content = content.replace("{AGENT_SCRIPT}", agent_script_command)
|
||||||
|
|
||||||
|
# 4. Strip scripts: and agent_scripts: sections from frontmatter
|
||||||
|
lines = content.splitlines(keepends=True)
|
||||||
|
output_lines: list[str] = []
|
||||||
|
in_frontmatter = False
|
||||||
|
skip_section = False
|
||||||
|
dash_count = 0
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.rstrip("\n\r")
|
||||||
|
if stripped == "---":
|
||||||
|
dash_count += 1
|
||||||
|
if dash_count == 1:
|
||||||
|
in_frontmatter = True
|
||||||
|
else:
|
||||||
|
in_frontmatter = False
|
||||||
|
skip_section = False
|
||||||
|
output_lines.append(line)
|
||||||
|
continue
|
||||||
|
if in_frontmatter:
|
||||||
|
if stripped in ("scripts:", "agent_scripts:"):
|
||||||
|
skip_section = True
|
||||||
|
continue
|
||||||
|
if skip_section:
|
||||||
|
if line[0:1].isspace():
|
||||||
|
continue # skip indented content under scripts/agent_scripts
|
||||||
|
skip_section = False
|
||||||
|
output_lines.append(line)
|
||||||
|
content = "".join(output_lines)
|
||||||
|
|
||||||
|
# 5. Replace {ARGS}
|
||||||
|
content = content.replace("{ARGS}", arg_placeholder)
|
||||||
|
|
||||||
|
# 6. Replace __AGENT__
|
||||||
|
content = content.replace("__AGENT__", agent_name)
|
||||||
|
|
||||||
|
# 7. Rewrite paths — delegate to the shared implementation in
|
||||||
|
# CommandRegistrar so extension-local paths are preserved and
|
||||||
|
# boundary rules stay consistent across the codebase.
|
||||||
|
from specify_cli.agents import CommandRegistrar
|
||||||
|
content = CommandRegistrar.rewrite_project_relative_paths(content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Install integration command files into *project_root*.
|
||||||
|
|
||||||
|
Returns the list of files created. Copies raw templates without
|
||||||
|
processing. Integrations that need placeholder replacement
|
||||||
|
(e.g. ``{SCRIPT}``, ``__AGENT__``) should override ``setup()``
|
||||||
|
and call ``process_template()`` in their own loop — see
|
||||||
|
``CopilotIntegration`` for an example.
|
||||||
|
"""
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
dest = self.commands_dest(project_root).resolve()
|
||||||
|
try:
|
||||||
|
dest.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration destination {dest} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
for src_file in templates:
|
||||||
|
dst_name = self.command_filename(src_file.stem)
|
||||||
|
dst_file = self.copy_command_to_directory(src_file, dest, dst_name)
|
||||||
|
self.record_file_in_manifest(dst_file, project_root, manifest)
|
||||||
|
created.append(dst_file)
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
def teardown(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
*,
|
||||||
|
force: bool = False,
|
||||||
|
) -> tuple[list[Path], list[Path]]:
|
||||||
|
"""Uninstall integration files from *project_root*.
|
||||||
|
|
||||||
|
Delegates to ``manifest.uninstall()`` which only removes files
|
||||||
|
whose hash still matches the recorded value (unless *force*).
|
||||||
|
|
||||||
|
Returns ``(removed, skipped)`` file lists.
|
||||||
|
"""
|
||||||
|
return manifest.uninstall(project_root, force=force)
|
||||||
|
|
||||||
|
# -- Convenience helpers for subclasses -------------------------------
|
||||||
|
|
||||||
|
def install(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""High-level install — calls ``setup()`` and returns created files."""
|
||||||
|
return self.setup(
|
||||||
|
project_root, manifest, parsed_options=parsed_options, **opts
|
||||||
|
)
|
||||||
|
|
||||||
|
def uninstall(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
*,
|
||||||
|
force: bool = False,
|
||||||
|
) -> tuple[list[Path], list[Path]]:
|
||||||
|
"""High-level uninstall — calls ``teardown()``."""
|
||||||
|
return self.teardown(project_root, manifest, force=force)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MarkdownIntegration — covers ~20 standard agents
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MarkdownIntegration(IntegrationBase):
|
||||||
|
"""Concrete base for integrations that use standard Markdown commands.
|
||||||
|
|
||||||
|
Subclasses only need to set ``key``, ``config``, ``registrar_config``
|
||||||
|
(and optionally ``context_file``). Everything else is inherited.
|
||||||
|
|
||||||
|
``setup()`` processes command templates (replacing ``{SCRIPT}``,
|
||||||
|
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
|
||||||
|
integration-specific scripts (``update-context.sh`` / ``.ps1``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
dest = self.commands_dest(project_root).resolve()
|
||||||
|
try:
|
||||||
|
dest.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration destination {dest} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
script_type = opts.get("script_type", "sh")
|
||||||
|
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS"
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
for src_file in templates:
|
||||||
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
|
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||||
|
dst_name = self.command_filename(src_file.stem)
|
||||||
|
dst_file = self.write_file_and_record(
|
||||||
|
processed, dest / dst_name, project_root, manifest
|
||||||
|
)
|
||||||
|
created.append(dst_file)
|
||||||
|
|
||||||
|
created.extend(self.install_scripts(project_root, manifest))
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TomlIntegration — TOML-format agents (Gemini, Tabnine)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TomlIntegration(IntegrationBase):
|
||||||
|
"""Concrete base for integrations that use TOML command format.
|
||||||
|
|
||||||
|
Mirrors ``MarkdownIntegration`` closely: subclasses only need to set
|
||||||
|
``key``, ``config``, ``registrar_config`` (and optionally
|
||||||
|
``context_file``). Everything else is inherited.
|
||||||
|
|
||||||
|
``setup()`` processes command templates through the same placeholder
|
||||||
|
pipeline as ``MarkdownIntegration``, then converts the result to
|
||||||
|
TOML format (``description`` key + ``prompt`` multiline string).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def command_filename(self, template_name: str) -> str:
|
||||||
|
"""TOML commands use ``.toml`` extension."""
|
||||||
|
return f"speckit.{template_name}.toml"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_description(content: str) -> str:
|
||||||
|
"""Extract the ``description`` value from YAML frontmatter.
|
||||||
|
|
||||||
|
Scans lines between the first pair of ``---`` delimiters for a
|
||||||
|
top-level ``description:`` key. Returns the value (with
|
||||||
|
surrounding quotes stripped) or an empty string if not found.
|
||||||
|
"""
|
||||||
|
in_frontmatter = False
|
||||||
|
for line in content.splitlines():
|
||||||
|
stripped = line.rstrip("\n\r")
|
||||||
|
if stripped == "---":
|
||||||
|
if not in_frontmatter:
|
||||||
|
in_frontmatter = True
|
||||||
|
continue
|
||||||
|
break # second ---
|
||||||
|
if in_frontmatter and stripped.startswith("description:"):
|
||||||
|
_, _, value = stripped.partition(":")
|
||||||
|
return value.strip().strip('"').strip("'")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _render_toml(description: str, body: str) -> str:
|
||||||
|
"""Render a TOML command file from description and body.
|
||||||
|
|
||||||
|
Uses multiline basic strings (``\"\"\"``) with backslashes
|
||||||
|
escaped, matching the output of the release script. Falls back
|
||||||
|
to multiline literal strings (``'''``) if the body contains
|
||||||
|
``\"\"\"``, then to an escaped basic string as a last resort.
|
||||||
|
|
||||||
|
The body is rstrip'd so the closing delimiter appears on the line
|
||||||
|
immediately after the last content line — matching the release
|
||||||
|
script's ``echo "$body"; echo '\"\"\"'`` pattern.
|
||||||
|
"""
|
||||||
|
toml_lines: list[str] = []
|
||||||
|
|
||||||
|
if description:
|
||||||
|
desc = description.replace('"', '\\"')
|
||||||
|
toml_lines.append(f'description = "{desc}"')
|
||||||
|
toml_lines.append("")
|
||||||
|
|
||||||
|
body = body.rstrip("\n")
|
||||||
|
|
||||||
|
# Escape backslashes for basic multiline strings.
|
||||||
|
escaped = body.replace("\\", "\\\\")
|
||||||
|
|
||||||
|
if '"""' not in escaped:
|
||||||
|
toml_lines.append('prompt = """')
|
||||||
|
toml_lines.append(escaped)
|
||||||
|
toml_lines.append('"""')
|
||||||
|
elif "'''" not in body:
|
||||||
|
toml_lines.append("prompt = '''")
|
||||||
|
toml_lines.append(body)
|
||||||
|
toml_lines.append("'''")
|
||||||
|
else:
|
||||||
|
escaped_body = (
|
||||||
|
body.replace("\\", "\\\\")
|
||||||
|
.replace('"', '\\"')
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
)
|
||||||
|
toml_lines.append(f'prompt = "{escaped_body}"')
|
||||||
|
|
||||||
|
return "\n".join(toml_lines) + "\n"
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
dest = self.commands_dest(project_root).resolve()
|
||||||
|
try:
|
||||||
|
dest.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration destination {dest} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
script_type = opts.get("script_type", "sh")
|
||||||
|
arg_placeholder = self.registrar_config.get("args", "{{args}}") if self.registrar_config else "{{args}}"
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
for src_file in templates:
|
||||||
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
|
description = self._extract_description(raw)
|
||||||
|
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||||
|
toml_content = self._render_toml(description, processed)
|
||||||
|
dst_name = self.command_filename(src_file.stem)
|
||||||
|
dst_file = self.write_file_and_record(
|
||||||
|
toml_content, dest / dst_name, project_root, manifest
|
||||||
|
)
|
||||||
|
created.append(dst_file)
|
||||||
|
|
||||||
|
created.extend(self.install_scripts(project_root, manifest))
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SkillsIntegration — skills-format agents (Codex, Kimi, Agy)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class SkillsIntegration(IntegrationBase):
|
||||||
|
"""Concrete base for integrations that install commands as agent skills.
|
||||||
|
|
||||||
|
Skills use the ``speckit-<name>/SKILL.md`` directory layout following
|
||||||
|
the `agentskills.io <https://agentskills.io/specification>`_ spec.
|
||||||
|
|
||||||
|
Subclasses set ``key``, ``config``, ``registrar_config`` (and
|
||||||
|
optionally ``context_file``) like any integration. They may also
|
||||||
|
override ``options()`` to declare additional CLI flags (e.g.
|
||||||
|
``--skills``, ``--migrate-legacy``).
|
||||||
|
|
||||||
|
``setup()`` processes each shared command template into a
|
||||||
|
``speckit-<name>/SKILL.md`` file with skills-oriented frontmatter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def skills_dest(self, project_root: Path) -> Path:
|
||||||
|
"""Return the absolute path to the skills output directory.
|
||||||
|
|
||||||
|
Derived from ``config["folder"]`` and the configured
|
||||||
|
``commands_subdir`` (defaults to ``"skills"``).
|
||||||
|
|
||||||
|
Raises ``ValueError`` when ``config`` or ``folder`` is missing.
|
||||||
|
"""
|
||||||
|
if not self.config:
|
||||||
|
raise ValueError(
|
||||||
|
f"{type(self).__name__}.config is not set."
|
||||||
|
)
|
||||||
|
folder = self.config.get("folder")
|
||||||
|
if not folder:
|
||||||
|
raise ValueError(
|
||||||
|
f"{type(self).__name__}.config is missing required 'folder' entry."
|
||||||
|
)
|
||||||
|
subdir = self.config.get("commands_subdir", "skills")
|
||||||
|
return project_root / folder / subdir
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Install command templates as agent skills.
|
||||||
|
|
||||||
|
Creates ``speckit-<name>/SKILL.md`` for each shared command
|
||||||
|
template. Each SKILL.md has normalised frontmatter containing
|
||||||
|
``name``, ``description``, ``compatibility``, and ``metadata``.
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
skills_dir = self.skills_dest(project_root).resolve()
|
||||||
|
try:
|
||||||
|
skills_dir.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Skills destination {skills_dir} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
script_type = opts.get("script_type", "sh")
|
||||||
|
arg_placeholder = (
|
||||||
|
self.registrar_config.get("args", "$ARGUMENTS")
|
||||||
|
if self.registrar_config
|
||||||
|
else "$ARGUMENTS"
|
||||||
|
)
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
for src_file in templates:
|
||||||
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Derive the skill name from the template stem
|
||||||
|
command_name = src_file.stem # e.g. "plan"
|
||||||
|
skill_name = f"speckit-{command_name.replace('.', '-')}"
|
||||||
|
|
||||||
|
# Parse frontmatter for description
|
||||||
|
frontmatter: dict[str, Any] = {}
|
||||||
|
if raw.startswith("---"):
|
||||||
|
parts = raw.split("---", 2)
|
||||||
|
if len(parts) >= 3:
|
||||||
|
try:
|
||||||
|
fm = yaml.safe_load(parts[1])
|
||||||
|
if isinstance(fm, dict):
|
||||||
|
frontmatter = fm
|
||||||
|
except yaml.YAMLError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Process body through the standard template pipeline
|
||||||
|
processed_body = self.process_template(
|
||||||
|
raw, self.key, script_type, arg_placeholder
|
||||||
|
)
|
||||||
|
# Strip the processed frontmatter — we rebuild it for skills.
|
||||||
|
# Preserve leading whitespace in the body to match release ZIP
|
||||||
|
# output byte-for-byte (the template body starts with \n after
|
||||||
|
# the closing ---).
|
||||||
|
if processed_body.startswith("---"):
|
||||||
|
parts = processed_body.split("---", 2)
|
||||||
|
if len(parts) >= 3:
|
||||||
|
processed_body = parts[2]
|
||||||
|
|
||||||
|
# Select description — use the original template description
|
||||||
|
# to stay byte-for-byte identical with release ZIP output.
|
||||||
|
description = frontmatter.get("description", "")
|
||||||
|
if not description:
|
||||||
|
description = f"Spec Kit: {command_name} workflow"
|
||||||
|
|
||||||
|
# Build SKILL.md with manually formatted frontmatter to match
|
||||||
|
# the release packaging script output exactly (double-quoted
|
||||||
|
# values, no yaml.safe_dump quoting differences).
|
||||||
|
def _quote(v: str) -> str:
|
||||||
|
escaped = v.replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
return f'"{escaped}"'
|
||||||
|
|
||||||
|
skill_content = (
|
||||||
|
f"---\n"
|
||||||
|
f"name: {_quote(skill_name)}\n"
|
||||||
|
f"description: {_quote(description)}\n"
|
||||||
|
f"compatibility: {_quote('Requires spec-kit project structure with .specify/ directory')}\n"
|
||||||
|
f"metadata:\n"
|
||||||
|
f" author: {_quote('github-spec-kit')}\n"
|
||||||
|
f" source: {_quote('templates/commands/' + src_file.name)}\n"
|
||||||
|
f"---\n"
|
||||||
|
f"{processed_body}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write speckit-<name>/SKILL.md
|
||||||
|
skill_dir = skills_dir / skill_name
|
||||||
|
skill_file = skill_dir / "SKILL.md"
|
||||||
|
dst = self.write_file_and_record(
|
||||||
|
skill_content, skill_file, project_root, manifest
|
||||||
|
)
|
||||||
|
created.append(dst)
|
||||||
|
|
||||||
|
created.extend(self.install_scripts(project_root, manifest))
|
||||||
|
return created
|
||||||
21
src/specify_cli/integrations/bob/__init__.py
Normal file
21
src/specify_cli/integrations/bob/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""IBM Bob integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class BobIntegration(MarkdownIntegration):
|
||||||
|
key = "bob"
|
||||||
|
config = {
|
||||||
|
"name": "IBM Bob",
|
||||||
|
"folder": ".bob/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": None,
|
||||||
|
"requires_cli": False,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".bob/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
23
src/specify_cli/integrations/bob/scripts/update-context.ps1
Normal file
23
src/specify_cli/integrations/bob/scripts/update-context.ps1
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — IBM Bob integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType bob
|
||||||
28
src/specify_cli/integrations/bob/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/bob/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — IBM Bob integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" bob
|
||||||
109
src/specify_cli/integrations/claude/__init__.py
Normal file
109
src/specify_cli/integrations/claude/__init__.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""Claude Code integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from ..base import SkillsIntegration
|
||||||
|
from ..manifest import IntegrationManifest
|
||||||
|
|
||||||
|
|
||||||
|
class ClaudeIntegration(SkillsIntegration):
|
||||||
|
"""Integration for Claude Code skills."""
|
||||||
|
|
||||||
|
key = "claude"
|
||||||
|
config = {
|
||||||
|
"name": "Claude Code",
|
||||||
|
"folder": ".claude/",
|
||||||
|
"commands_subdir": "skills",
|
||||||
|
"install_url": "https://docs.anthropic.com/en/docs/claude-code/setup",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".claude/skills",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": "/SKILL.md",
|
||||||
|
}
|
||||||
|
context_file = "CLAUDE.md"
|
||||||
|
|
||||||
|
def command_filename(self, template_name: str) -> str:
|
||||||
|
"""Claude skills live at .claude/skills/<name>/SKILL.md."""
|
||||||
|
skill_name = f"speckit-{template_name.replace('.', '-')}"
|
||||||
|
return f"{skill_name}/SKILL.md"
|
||||||
|
|
||||||
|
def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
|
||||||
|
"""Render a processed command template as a Claude skill."""
|
||||||
|
skill_name = f"speckit-{template_name.replace('.', '-')}"
|
||||||
|
description = frontmatter.get(
|
||||||
|
"description",
|
||||||
|
f"Spec-kit workflow command: {template_name}",
|
||||||
|
)
|
||||||
|
skill_frontmatter = self._build_skill_fm(
|
||||||
|
skill_name, description, f"templates/commands/{template_name}.md"
|
||||||
|
)
|
||||||
|
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
|
||||||
|
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
|
||||||
|
|
||||||
|
def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
|
||||||
|
from specify_cli.agents import CommandRegistrar
|
||||||
|
return CommandRegistrar.build_skill_frontmatter(
|
||||||
|
self.key, name, description, source
|
||||||
|
)
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Install Claude skills into .claude/skills."""
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
dest = self.skills_dest(project_root).resolve()
|
||||||
|
try:
|
||||||
|
dest.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration destination {dest} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
script_type = opts.get("script_type", "sh")
|
||||||
|
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
|
||||||
|
from specify_cli.agents import CommandRegistrar
|
||||||
|
registrar = CommandRegistrar()
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
for src_file in templates:
|
||||||
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
|
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||||
|
frontmatter, body = registrar.parse_frontmatter(processed)
|
||||||
|
if not isinstance(frontmatter, dict):
|
||||||
|
frontmatter = {}
|
||||||
|
|
||||||
|
rendered = self._render_skill(src_file.stem, frontmatter, body)
|
||||||
|
dst_file = self.write_file_and_record(
|
||||||
|
rendered,
|
||||||
|
dest / self.command_filename(src_file.stem),
|
||||||
|
project_root,
|
||||||
|
manifest,
|
||||||
|
)
|
||||||
|
created.append(dst_file)
|
||||||
|
|
||||||
|
created.extend(self.install_scripts(project_root, manifest))
|
||||||
|
return created
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Claude Code integration: create/update CLAUDE.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType claude
|
||||||
28
src/specify_cli/integrations/claude/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/claude/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Claude Code integration: create/update CLAUDE.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" claude
|
||||||
21
src/specify_cli/integrations/codebuddy/__init__.py
Normal file
21
src/specify_cli/integrations/codebuddy/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""CodeBuddy CLI integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class CodebuddyIntegration(MarkdownIntegration):
|
||||||
|
key = "codebuddy"
|
||||||
|
config = {
|
||||||
|
"name": "CodeBuddy",
|
||||||
|
"folder": ".codebuddy/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://www.codebuddy.ai/cli",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".codebuddy/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "CODEBUDDY.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — CodeBuddy integration: create/update CODEBUDDY.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codebuddy
|
||||||
28
src/specify_cli/integrations/codebuddy/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/codebuddy/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — CodeBuddy integration: create/update CODEBUDDY.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codebuddy
|
||||||
40
src/specify_cli/integrations/codex/__init__.py
Normal file
40
src/specify_cli/integrations/codex/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Codex CLI integration — skills-based agent.
|
||||||
|
|
||||||
|
Codex uses the ``.agents/skills/speckit-<name>/SKILL.md`` layout.
|
||||||
|
Commands are deprecated; ``--skills`` defaults to ``True``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..base import IntegrationOption, SkillsIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class CodexIntegration(SkillsIntegration):
|
||||||
|
"""Integration for OpenAI Codex CLI."""
|
||||||
|
|
||||||
|
key = "codex"
|
||||||
|
config = {
|
||||||
|
"name": "Codex CLI",
|
||||||
|
"folder": ".agents/",
|
||||||
|
"commands_subdir": "skills",
|
||||||
|
"install_url": "https://github.com/openai/codex",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".agents/skills",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": "/SKILL.md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def options(cls) -> list[IntegrationOption]:
|
||||||
|
return [
|
||||||
|
IntegrationOption(
|
||||||
|
"--skills",
|
||||||
|
is_flag=True,
|
||||||
|
default=True,
|
||||||
|
help="Install as agent skills (default for Codex)",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# update-context.ps1 — Codex CLI integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codex
|
||||||
24
src/specify_cli/integrations/codex/scripts/update-context.sh
Executable file
24
src/specify_cli/integrations/codex/scripts/update-context.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Codex CLI integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codex
|
||||||
185
src/specify_cli/integrations/copilot/__init__.py
Normal file
185
src/specify_cli/integrations/copilot/__init__.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"""Copilot integration — GitHub Copilot in VS Code.
|
||||||
|
|
||||||
|
Copilot has several unique behaviors compared to standard markdown agents:
|
||||||
|
- Commands use ``.agent.md`` extension (not ``.md``)
|
||||||
|
- Each command gets a companion ``.prompt.md`` file in ``.github/prompts/``
|
||||||
|
- Installs ``.vscode/settings.json`` with prompt file recommendations
|
||||||
|
- Context file lives at ``.github/copilot-instructions.md``
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..base import IntegrationBase
|
||||||
|
from ..manifest import IntegrationManifest
|
||||||
|
|
||||||
|
|
||||||
|
class CopilotIntegration(IntegrationBase):
|
||||||
|
"""Integration for GitHub Copilot in VS Code."""
|
||||||
|
|
||||||
|
key = "copilot"
|
||||||
|
config = {
|
||||||
|
"name": "GitHub Copilot",
|
||||||
|
"folder": ".github/",
|
||||||
|
"commands_subdir": "agents",
|
||||||
|
"install_url": None,
|
||||||
|
"requires_cli": False,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".github/agents",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".agent.md",
|
||||||
|
}
|
||||||
|
context_file = ".github/copilot-instructions.md"
|
||||||
|
|
||||||
|
def command_filename(self, template_name: str) -> str:
|
||||||
|
"""Copilot commands use ``.agent.md`` extension."""
|
||||||
|
return f"speckit.{template_name}.agent.md"
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Install copilot commands, companion prompts, and VS Code settings.
|
||||||
|
|
||||||
|
Uses base class primitives to: read templates, process them
|
||||||
|
(replace placeholders, strip script blocks, rewrite paths),
|
||||||
|
write as ``.agent.md``, then add companion prompts and VS Code settings.
|
||||||
|
"""
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
dest = self.commands_dest(project_root)
|
||||||
|
dest_resolved = dest.resolve()
|
||||||
|
try:
|
||||||
|
dest_resolved.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration destination {dest_resolved} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
script_type = opts.get("script_type", "sh")
|
||||||
|
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
|
||||||
|
|
||||||
|
# 1. Process and write command files as .agent.md
|
||||||
|
for src_file in templates:
|
||||||
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
|
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||||
|
dst_name = self.command_filename(src_file.stem)
|
||||||
|
dst_file = self.write_file_and_record(
|
||||||
|
processed, dest / dst_name, project_root, manifest
|
||||||
|
)
|
||||||
|
created.append(dst_file)
|
||||||
|
|
||||||
|
# 2. Generate companion .prompt.md files from the templates we just wrote
|
||||||
|
prompts_dir = project_root / ".github" / "prompts"
|
||||||
|
for src_file in templates:
|
||||||
|
cmd_name = f"speckit.{src_file.stem}"
|
||||||
|
prompt_content = f"---\nagent: {cmd_name}\n---\n"
|
||||||
|
prompt_file = self.write_file_and_record(
|
||||||
|
prompt_content,
|
||||||
|
prompts_dir / f"{cmd_name}.prompt.md",
|
||||||
|
project_root,
|
||||||
|
manifest,
|
||||||
|
)
|
||||||
|
created.append(prompt_file)
|
||||||
|
|
||||||
|
# Write .vscode/settings.json
|
||||||
|
settings_src = self._vscode_settings_path()
|
||||||
|
if settings_src and settings_src.is_file():
|
||||||
|
dst_settings = project_root / ".vscode" / "settings.json"
|
||||||
|
dst_settings.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if dst_settings.exists():
|
||||||
|
# Merge into existing — don't track since we can't safely
|
||||||
|
# remove the user's settings file on uninstall.
|
||||||
|
self._merge_vscode_settings(settings_src, dst_settings)
|
||||||
|
else:
|
||||||
|
shutil.copy2(settings_src, dst_settings)
|
||||||
|
self.record_file_in_manifest(dst_settings, project_root, manifest)
|
||||||
|
created.append(dst_settings)
|
||||||
|
|
||||||
|
# 4. Install integration-specific update-context scripts
|
||||||
|
created.extend(self.install_scripts(project_root, manifest))
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
def _vscode_settings_path(self) -> Path | None:
|
||||||
|
"""Return path to the bundled vscode-settings.json template."""
|
||||||
|
tpl_dir = self.shared_templates_dir()
|
||||||
|
if tpl_dir:
|
||||||
|
candidate = tpl_dir / "vscode-settings.json"
|
||||||
|
if candidate.is_file():
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _merge_vscode_settings(src: Path, dst: Path) -> None:
|
||||||
|
"""Merge settings from *src* into existing *dst* JSON file.
|
||||||
|
|
||||||
|
Top-level keys from *src* are added only if missing in *dst*.
|
||||||
|
For dict-valued keys, sub-keys are merged the same way.
|
||||||
|
|
||||||
|
If *dst* cannot be parsed (e.g. JSONC with comments), the merge
|
||||||
|
is skipped to avoid overwriting user settings.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
existing = json.loads(dst.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
# Cannot parse existing file (likely JSONC with comments).
|
||||||
|
# Skip merge to preserve the user's settings, but show
|
||||||
|
# what they should add manually.
|
||||||
|
import logging
|
||||||
|
template_content = src.read_text(encoding="utf-8")
|
||||||
|
logging.getLogger(__name__).warning(
|
||||||
|
"Could not parse %s (may contain JSONC comments). "
|
||||||
|
"Skipping settings merge to preserve existing file.\n"
|
||||||
|
"Please add the following settings manually:\n%s",
|
||||||
|
dst, template_content,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
new_settings = json.loads(src.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
if not isinstance(existing, dict) or not isinstance(new_settings, dict):
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).warning(
|
||||||
|
"Skipping settings merge: %s or template is not a JSON object.", dst
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
for key, value in new_settings.items():
|
||||||
|
if key not in existing:
|
||||||
|
existing[key] = value
|
||||||
|
changed = True
|
||||||
|
elif isinstance(existing[key], dict) and isinstance(value, dict):
|
||||||
|
for sub_key, sub_value in value.items():
|
||||||
|
if sub_key not in existing[key]:
|
||||||
|
existing[key][sub_key] = sub_value
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return
|
||||||
|
|
||||||
|
dst.write_text(
|
||||||
|
json.dumps(existing, indent=4) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# update-context.ps1 — Copilot integration: create/update .github/copilot-instructions.md
|
||||||
|
#
|
||||||
|
# This is the copilot-specific implementation that produces the GitHub
|
||||||
|
# Copilot instructions file. The shared dispatcher reads
|
||||||
|
# .specify/integration.json and calls this script.
|
||||||
|
#
|
||||||
|
# NOTE: This script is not yet active. It will be activated in Stage 7
|
||||||
|
# when the shared update-agent-context.ps1 replaces its switch statement
|
||||||
|
# with integration.json-based dispatch. The shared script must also be
|
||||||
|
# refactored to support SPECKIT_SOURCE_ONLY (guard the Main call) before
|
||||||
|
# dot-sourcing will work.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Invoke shared update-agent-context script as a separate process.
|
||||||
|
# Dot-sourcing is unsafe until that script guards its Main call.
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType copilot
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Copilot integration: create/update .github/copilot-instructions.md
|
||||||
|
#
|
||||||
|
# This is the copilot-specific implementation that produces the GitHub
|
||||||
|
# Copilot instructions file. The shared dispatcher reads
|
||||||
|
# .specify/integration.json and calls this script.
|
||||||
|
#
|
||||||
|
# NOTE: This script is not yet active. It will be activated in Stage 7
|
||||||
|
# when the shared update-agent-context.sh replaces its case statement
|
||||||
|
# with integration.json-based dispatch. The shared script must also be
|
||||||
|
# refactored to support SPECKIT_SOURCE_ONLY (guard the main logic)
|
||||||
|
# before sourcing will work.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Invoke shared update-agent-context script as a separate process.
|
||||||
|
# Sourcing is unsafe until that script guards its main logic.
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" copilot
|
||||||
21
src/specify_cli/integrations/cursor_agent/__init__.py
Normal file
21
src/specify_cli/integrations/cursor_agent/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Cursor IDE integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class CursorAgentIntegration(MarkdownIntegration):
|
||||||
|
key = "cursor-agent"
|
||||||
|
config = {
|
||||||
|
"name": "Cursor",
|
||||||
|
"folder": ".cursor/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": None,
|
||||||
|
"requires_cli": False,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".cursor/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = ".cursor/rules/specify-rules.mdc"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Cursor integration: create/update .cursor/rules/specify-rules.mdc
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType cursor-agent
|
||||||
28
src/specify_cli/integrations/cursor_agent/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/cursor_agent/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Cursor integration: create/update .cursor/rules/specify-rules.mdc
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" cursor-agent
|
||||||
21
src/specify_cli/integrations/gemini/__init__.py
Normal file
21
src/specify_cli/integrations/gemini/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Gemini CLI integration."""
|
||||||
|
|
||||||
|
from ..base import TomlIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiIntegration(TomlIntegration):
|
||||||
|
key = "gemini"
|
||||||
|
config = {
|
||||||
|
"name": "Gemini CLI",
|
||||||
|
"folder": ".gemini/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://github.com/google-gemini/gemini-cli",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".gemini/commands",
|
||||||
|
"format": "toml",
|
||||||
|
"args": "{{args}}",
|
||||||
|
"extension": ".toml",
|
||||||
|
}
|
||||||
|
context_file = "GEMINI.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Gemini CLI integration: create/update GEMINI.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType gemini
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Gemini CLI integration: create/update GEMINI.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" gemini
|
||||||
133
src/specify_cli/integrations/generic/__init__.py
Normal file
133
src/specify_cli/integrations/generic/__init__.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""Generic integration — bring your own agent.
|
||||||
|
|
||||||
|
Requires ``--commands-dir`` to specify the output directory for command
|
||||||
|
files. No longer special-cased in the core CLI — just another
|
||||||
|
integration with its own required option.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..base import IntegrationOption, MarkdownIntegration
|
||||||
|
from ..manifest import IntegrationManifest
|
||||||
|
|
||||||
|
|
||||||
|
class GenericIntegration(MarkdownIntegration):
|
||||||
|
"""Integration for user-specified (generic) agents."""
|
||||||
|
|
||||||
|
key = "generic"
|
||||||
|
config = {
|
||||||
|
"name": "Generic (bring your own agent)",
|
||||||
|
"folder": None, # Set dynamically from --commands-dir
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": None,
|
||||||
|
"requires_cli": False,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": "", # Set dynamically from --commands-dir
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def options(cls) -> list[IntegrationOption]:
|
||||||
|
return [
|
||||||
|
IntegrationOption(
|
||||||
|
"--commands-dir",
|
||||||
|
required=True,
|
||||||
|
help="Directory for command files (e.g. .myagent/commands/)",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_commands_dir(
|
||||||
|
parsed_options: dict[str, Any] | None,
|
||||||
|
opts: dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
"""Extract ``--commands-dir`` from parsed options or raw_options.
|
||||||
|
|
||||||
|
Returns the directory string or raises ``ValueError``.
|
||||||
|
"""
|
||||||
|
parsed_options = parsed_options or {}
|
||||||
|
|
||||||
|
commands_dir = parsed_options.get("commands_dir")
|
||||||
|
if commands_dir:
|
||||||
|
return commands_dir
|
||||||
|
|
||||||
|
# Fall back to raw_options (--integration-options="--commands-dir ...")
|
||||||
|
raw = opts.get("raw_options")
|
||||||
|
if raw:
|
||||||
|
import shlex
|
||||||
|
tokens = shlex.split(raw)
|
||||||
|
for i, token in enumerate(tokens):
|
||||||
|
if token == "--commands-dir" and i + 1 < len(tokens):
|
||||||
|
return tokens[i + 1]
|
||||||
|
if token.startswith("--commands-dir="):
|
||||||
|
return token.split("=", 1)[1]
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"--commands-dir is required for the generic integration"
|
||||||
|
)
|
||||||
|
|
||||||
|
def commands_dest(self, project_root: Path) -> Path:
|
||||||
|
"""Not supported for GenericIntegration — use setup() directly.
|
||||||
|
|
||||||
|
GenericIntegration is stateless; the output directory comes from
|
||||||
|
``parsed_options`` or ``raw_options`` at call time, not from
|
||||||
|
instance state.
|
||||||
|
"""
|
||||||
|
raise ValueError(
|
||||||
|
"GenericIntegration.commands_dest() cannot be called directly; "
|
||||||
|
"the output directory is resolved from parsed_options in setup()"
|
||||||
|
)
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Install commands to the user-provided commands directory."""
|
||||||
|
commands_dir = self._resolve_commands_dir(parsed_options, opts)
|
||||||
|
|
||||||
|
templates = self.list_command_templates()
|
||||||
|
if not templates:
|
||||||
|
return []
|
||||||
|
|
||||||
|
project_root_resolved = project_root.resolve()
|
||||||
|
if manifest.project_root != project_root_resolved:
|
||||||
|
raise ValueError(
|
||||||
|
f"manifest.project_root ({manifest.project_root}) does not match "
|
||||||
|
f"project_root ({project_root_resolved})"
|
||||||
|
)
|
||||||
|
|
||||||
|
dest = (project_root / commands_dir).resolve()
|
||||||
|
try:
|
||||||
|
dest.relative_to(project_root_resolved)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration destination {dest} escapes "
|
||||||
|
f"project root {project_root_resolved}"
|
||||||
|
) from exc
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
script_type = opts.get("script_type", "sh")
|
||||||
|
arg_placeholder = "$ARGUMENTS"
|
||||||
|
created: list[Path] = []
|
||||||
|
|
||||||
|
for src_file in templates:
|
||||||
|
raw = src_file.read_text(encoding="utf-8")
|
||||||
|
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
||||||
|
dst_name = self.command_filename(src_file.stem)
|
||||||
|
dst_file = self.write_file_and_record(
|
||||||
|
processed, dest / dst_name, project_root, manifest
|
||||||
|
)
|
||||||
|
created.append(dst_file)
|
||||||
|
|
||||||
|
created.extend(self.install_scripts(project_root, manifest))
|
||||||
|
return created
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# update-context.ps1 — Generic integration: create/update context file
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType generic
|
||||||
24
src/specify_cli/integrations/generic/scripts/update-context.sh
Executable file
24
src/specify_cli/integrations/generic/scripts/update-context.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Generic integration: create/update context file
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" generic
|
||||||
21
src/specify_cli/integrations/iflow/__init__.py
Normal file
21
src/specify_cli/integrations/iflow/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""iFlow CLI integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class IflowIntegration(MarkdownIntegration):
|
||||||
|
key = "iflow"
|
||||||
|
config = {
|
||||||
|
"name": "iFlow CLI",
|
||||||
|
"folder": ".iflow/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://docs.iflow.cn/en/cli/quickstart",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".iflow/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "IFLOW.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — iFlow CLI integration: create/update IFLOW.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType iflow
|
||||||
28
src/specify_cli/integrations/iflow/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/iflow/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — iFlow CLI integration: create/update IFLOW.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" iflow
|
||||||
21
src/specify_cli/integrations/junie/__init__.py
Normal file
21
src/specify_cli/integrations/junie/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Junie integration (JetBrains)."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class JunieIntegration(MarkdownIntegration):
|
||||||
|
key = "junie"
|
||||||
|
config = {
|
||||||
|
"name": "Junie",
|
||||||
|
"folder": ".junie/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://junie.jetbrains.com/",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".junie/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = ".junie/AGENTS.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Junie integration: create/update .junie/AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType junie
|
||||||
28
src/specify_cli/integrations/junie/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/junie/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Junie integration: create/update .junie/AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" junie
|
||||||
21
src/specify_cli/integrations/kilocode/__init__.py
Normal file
21
src/specify_cli/integrations/kilocode/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Kilo Code integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class KilocodeIntegration(MarkdownIntegration):
|
||||||
|
key = "kilocode"
|
||||||
|
config = {
|
||||||
|
"name": "Kilo Code",
|
||||||
|
"folder": ".kilocode/",
|
||||||
|
"commands_subdir": "workflows",
|
||||||
|
"install_url": None,
|
||||||
|
"requires_cli": False,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".kilocode/workflows",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = ".kilocode/rules/specify-rules.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Kilo Code integration: create/update .kilocode/rules/specify-rules.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kilocode
|
||||||
28
src/specify_cli/integrations/kilocode/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/kilocode/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Kilo Code integration: create/update .kilocode/rules/specify-rules.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kilocode
|
||||||
124
src/specify_cli/integrations/kimi/__init__.py
Normal file
124
src/specify_cli/integrations/kimi/__init__.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""Kimi Code integration — skills-based agent (Moonshot AI).
|
||||||
|
|
||||||
|
Kimi uses the ``.kimi/skills/speckit-<name>/SKILL.md`` layout with
|
||||||
|
``/skill:speckit-<name>`` invocation syntax.
|
||||||
|
|
||||||
|
Includes legacy migration logic for projects initialised before Kimi
|
||||||
|
moved from dotted skill directories (``speckit.xxx``) to hyphenated
|
||||||
|
(``speckit-xxx``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..base import IntegrationOption, SkillsIntegration
|
||||||
|
from ..manifest import IntegrationManifest
|
||||||
|
|
||||||
|
|
||||||
|
class KimiIntegration(SkillsIntegration):
|
||||||
|
"""Integration for Kimi Code CLI (Moonshot AI)."""
|
||||||
|
|
||||||
|
key = "kimi"
|
||||||
|
config = {
|
||||||
|
"name": "Kimi Code",
|
||||||
|
"folder": ".kimi/",
|
||||||
|
"commands_subdir": "skills",
|
||||||
|
"install_url": "https://code.kimi.com/",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".kimi/skills",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": "/SKILL.md",
|
||||||
|
}
|
||||||
|
context_file = "KIMI.md"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def options(cls) -> list[IntegrationOption]:
|
||||||
|
return [
|
||||||
|
IntegrationOption(
|
||||||
|
"--skills",
|
||||||
|
is_flag=True,
|
||||||
|
default=True,
|
||||||
|
help="Install as agent skills (default for Kimi)",
|
||||||
|
),
|
||||||
|
IntegrationOption(
|
||||||
|
"--migrate-legacy",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Migrate legacy dotted skill dirs (speckit.xxx → speckit-xxx)",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def setup(
|
||||||
|
self,
|
||||||
|
project_root: Path,
|
||||||
|
manifest: IntegrationManifest,
|
||||||
|
parsed_options: dict[str, Any] | None = None,
|
||||||
|
**opts: Any,
|
||||||
|
) -> list[Path]:
|
||||||
|
"""Install skills with optional legacy dotted-name migration."""
|
||||||
|
parsed_options = parsed_options or {}
|
||||||
|
|
||||||
|
# Run base setup first so hyphenated targets (speckit-*) exist,
|
||||||
|
# then migrate/clean legacy dotted dirs without risking user content loss.
|
||||||
|
created = super().setup(
|
||||||
|
project_root, manifest, parsed_options=parsed_options, **opts
|
||||||
|
)
|
||||||
|
|
||||||
|
if parsed_options.get("migrate_legacy", False):
|
||||||
|
skills_dir = self.skills_dest(project_root)
|
||||||
|
if skills_dir.is_dir():
|
||||||
|
_migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
|
||||||
|
"""Migrate legacy Kimi dotted skill dirs (speckit.xxx) to hyphenated format.
|
||||||
|
|
||||||
|
Returns ``(migrated_count, removed_count)``.
|
||||||
|
"""
|
||||||
|
if not skills_dir.is_dir():
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
migrated_count = 0
|
||||||
|
removed_count = 0
|
||||||
|
|
||||||
|
for legacy_dir in sorted(skills_dir.glob("speckit.*")):
|
||||||
|
if not legacy_dir.is_dir():
|
||||||
|
continue
|
||||||
|
if not (legacy_dir / "SKILL.md").exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
suffix = legacy_dir.name[len("speckit."):]
|
||||||
|
if not suffix:
|
||||||
|
continue
|
||||||
|
|
||||||
|
target_dir = skills_dir / f"speckit-{suffix.replace('.', '-')}"
|
||||||
|
|
||||||
|
if not target_dir.exists():
|
||||||
|
shutil.move(str(legacy_dir), str(target_dir))
|
||||||
|
migrated_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Target exists — only remove legacy if SKILL.md is identical
|
||||||
|
target_skill = target_dir / "SKILL.md"
|
||||||
|
legacy_skill = legacy_dir / "SKILL.md"
|
||||||
|
if target_skill.is_file():
|
||||||
|
try:
|
||||||
|
if target_skill.read_bytes() == legacy_skill.read_bytes():
|
||||||
|
has_extra = any(
|
||||||
|
child.name != "SKILL.md" for child in legacy_dir.iterdir()
|
||||||
|
)
|
||||||
|
if not has_extra:
|
||||||
|
shutil.rmtree(legacy_dir)
|
||||||
|
removed_count += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return (migrated_count, removed_count)
|
||||||
17
src/specify_cli/integrations/kimi/scripts/update-context.ps1
Normal file
17
src/specify_cli/integrations/kimi/scripts/update-context.ps1
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# update-context.ps1 — Kimi Code integration: create/update KIMI.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kimi
|
||||||
24
src/specify_cli/integrations/kimi/scripts/update-context.sh
Executable file
24
src/specify_cli/integrations/kimi/scripts/update-context.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Kimi Code integration: create/update KIMI.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kimi
|
||||||
21
src/specify_cli/integrations/kiro_cli/__init__.py
Normal file
21
src/specify_cli/integrations/kiro_cli/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Kiro CLI integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class KiroCliIntegration(MarkdownIntegration):
|
||||||
|
key = "kiro-cli"
|
||||||
|
config = {
|
||||||
|
"name": "Kiro CLI",
|
||||||
|
"folder": ".kiro/",
|
||||||
|
"commands_subdir": "prompts",
|
||||||
|
"install_url": "https://kiro.dev/docs/cli/",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".kiro/prompts",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Kiro CLI integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kiro-cli
|
||||||
28
src/specify_cli/integrations/kiro_cli/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/kiro_cli/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Kiro CLI integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kiro-cli
|
||||||
265
src/specify_cli/integrations/manifest.py
Normal file
265
src/specify_cli/integrations/manifest.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
"""Hash-tracked installation manifest for integrations.
|
||||||
|
|
||||||
|
Each installed integration records the files it created together with
|
||||||
|
their SHA-256 hashes. On uninstall only files whose hash still matches
|
||||||
|
the recorded value are removed — modified files are left in place and
|
||||||
|
reported to the caller.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
"""Return the hex SHA-256 digest of *path*."""
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
for chunk in iter(lambda: fh.read(8192), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_rel_path(rel: Path, root: Path) -> Path:
|
||||||
|
"""Resolve *rel* against *root* and verify it stays within *root*.
|
||||||
|
|
||||||
|
Raises ``ValueError`` if *rel* is absolute, contains ``..`` segments
|
||||||
|
that escape *root*, or otherwise resolves outside the project root.
|
||||||
|
"""
|
||||||
|
if rel.is_absolute():
|
||||||
|
raise ValueError(
|
||||||
|
f"Absolute paths are not allowed in manifests: {rel}"
|
||||||
|
)
|
||||||
|
resolved = (root / rel).resolve()
|
||||||
|
root_resolved = root.resolve()
|
||||||
|
try:
|
||||||
|
resolved.relative_to(root_resolved)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
f"Path {rel} resolves to {resolved} which is outside "
|
||||||
|
f"the project root {root_resolved}"
|
||||||
|
) from None
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationManifest:
|
||||||
|
"""Tracks files installed by a single integration.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
key: Integration identifier (e.g. ``"copilot"``).
|
||||||
|
project_root: Absolute path to the project directory.
|
||||||
|
version: CLI version string recorded in the manifest.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, key: str, project_root: Path, version: str = "") -> None:
|
||||||
|
self.key = key
|
||||||
|
self.project_root = project_root.resolve()
|
||||||
|
self.version = version
|
||||||
|
self._files: dict[str, str] = {} # rel_path → sha256 hex
|
||||||
|
self._installed_at: str = ""
|
||||||
|
|
||||||
|
# -- Manifest file location -------------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manifest_path(self) -> Path:
|
||||||
|
"""Path to the on-disk manifest JSON."""
|
||||||
|
return self.project_root / ".specify" / "integrations" / f"{self.key}.manifest.json"
|
||||||
|
|
||||||
|
# -- Recording files --------------------------------------------------
|
||||||
|
|
||||||
|
def record_file(self, rel_path: str | Path, content: bytes | str) -> Path:
|
||||||
|
"""Write *content* to *rel_path* (relative to project root) and record its hash.
|
||||||
|
|
||||||
|
Creates parent directories as needed. Returns the absolute path
|
||||||
|
of the written file.
|
||||||
|
|
||||||
|
Raises ``ValueError`` if *rel_path* resolves outside the project root.
|
||||||
|
"""
|
||||||
|
rel = Path(rel_path)
|
||||||
|
abs_path = _validate_rel_path(rel, self.project_root)
|
||||||
|
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if isinstance(content, str):
|
||||||
|
content = content.encode("utf-8")
|
||||||
|
abs_path.write_bytes(content)
|
||||||
|
|
||||||
|
normalized = abs_path.relative_to(self.project_root).as_posix()
|
||||||
|
self._files[normalized] = hashlib.sha256(content).hexdigest()
|
||||||
|
return abs_path
|
||||||
|
|
||||||
|
def record_existing(self, rel_path: str | Path) -> None:
|
||||||
|
"""Record the hash of an already-existing file at *rel_path*.
|
||||||
|
|
||||||
|
Raises ``ValueError`` if *rel_path* resolves outside the project root.
|
||||||
|
"""
|
||||||
|
rel = Path(rel_path)
|
||||||
|
abs_path = _validate_rel_path(rel, self.project_root)
|
||||||
|
normalized = abs_path.relative_to(self.project_root).as_posix()
|
||||||
|
self._files[normalized] = _sha256(abs_path)
|
||||||
|
|
||||||
|
# -- Querying ---------------------------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def files(self) -> dict[str, str]:
|
||||||
|
"""Return a copy of the ``{rel_path: sha256}`` mapping."""
|
||||||
|
return dict(self._files)
|
||||||
|
|
||||||
|
def check_modified(self) -> list[str]:
|
||||||
|
"""Return relative paths of tracked files whose content changed on disk."""
|
||||||
|
modified: list[str] = []
|
||||||
|
for rel, expected_hash in self._files.items():
|
||||||
|
rel_path = Path(rel)
|
||||||
|
# Skip paths that are absolute or attempt to escape the project root
|
||||||
|
if rel_path.is_absolute() or ".." in rel_path.parts:
|
||||||
|
continue
|
||||||
|
abs_path = self.project_root / rel_path
|
||||||
|
if not abs_path.exists() and not abs_path.is_symlink():
|
||||||
|
continue
|
||||||
|
# Treat symlinks and non-regular-files as modified
|
||||||
|
if abs_path.is_symlink() or not abs_path.is_file():
|
||||||
|
modified.append(rel)
|
||||||
|
continue
|
||||||
|
if _sha256(abs_path) != expected_hash:
|
||||||
|
modified.append(rel)
|
||||||
|
return modified
|
||||||
|
|
||||||
|
# -- Uninstall --------------------------------------------------------
|
||||||
|
|
||||||
|
def uninstall(
|
||||||
|
self,
|
||||||
|
project_root: Path | None = None,
|
||||||
|
*,
|
||||||
|
force: bool = False,
|
||||||
|
) -> tuple[list[Path], list[Path]]:
|
||||||
|
"""Remove tracked files whose hash still matches.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
project_root: Override for the project root.
|
||||||
|
force: If ``True``, remove files even if modified.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``(removed, skipped)`` — absolute paths.
|
||||||
|
"""
|
||||||
|
root = (project_root or self.project_root).resolve()
|
||||||
|
removed: list[Path] = []
|
||||||
|
skipped: list[Path] = []
|
||||||
|
|
||||||
|
for rel, expected_hash in self._files.items():
|
||||||
|
# Use non-resolved path for deletion so symlinks themselves
|
||||||
|
# are removed, not their targets.
|
||||||
|
path = root / rel
|
||||||
|
# Validate containment lexically (without following symlinks)
|
||||||
|
# by collapsing .. segments via Path resolution on the string parts.
|
||||||
|
try:
|
||||||
|
normed = Path(os.path.normpath(path))
|
||||||
|
normed.relative_to(root)
|
||||||
|
except (ValueError, OSError):
|
||||||
|
continue
|
||||||
|
if not path.exists() and not path.is_symlink():
|
||||||
|
continue
|
||||||
|
# Skip directories — manifest only tracks files
|
||||||
|
if not path.is_file() and not path.is_symlink():
|
||||||
|
skipped.append(path)
|
||||||
|
continue
|
||||||
|
# Never follow symlinks when comparing hashes. Only remove
|
||||||
|
# symlinks when forced, to avoid acting on tampered entries.
|
||||||
|
if path.is_symlink():
|
||||||
|
if not force:
|
||||||
|
skipped.append(path)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if not force and _sha256(path) != expected_hash:
|
||||||
|
skipped.append(path)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
path.unlink()
|
||||||
|
except OSError:
|
||||||
|
skipped.append(path)
|
||||||
|
continue
|
||||||
|
removed.append(path)
|
||||||
|
# Clean up empty parent directories up to project root
|
||||||
|
parent = path.parent
|
||||||
|
while parent != root:
|
||||||
|
try:
|
||||||
|
parent.rmdir() # only succeeds if empty
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
parent = parent.parent
|
||||||
|
|
||||||
|
# Remove the manifest file itself
|
||||||
|
manifest = root / ".specify" / "integrations" / f"{self.key}.manifest.json"
|
||||||
|
if manifest.exists():
|
||||||
|
manifest.unlink()
|
||||||
|
parent = manifest.parent
|
||||||
|
while parent != root:
|
||||||
|
try:
|
||||||
|
parent.rmdir()
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
parent = parent.parent
|
||||||
|
|
||||||
|
return removed, skipped
|
||||||
|
|
||||||
|
# -- Persistence ------------------------------------------------------
|
||||||
|
|
||||||
|
def save(self) -> Path:
|
||||||
|
"""Write the manifest to disk. Returns the manifest path."""
|
||||||
|
self._installed_at = self._installed_at or datetime.now(timezone.utc).isoformat()
|
||||||
|
data: dict[str, Any] = {
|
||||||
|
"integration": self.key,
|
||||||
|
"version": self.version,
|
||||||
|
"installed_at": self._installed_at,
|
||||||
|
"files": self._files,
|
||||||
|
}
|
||||||
|
path = self.manifest_path
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, key: str, project_root: Path) -> IntegrationManifest:
|
||||||
|
"""Load an existing manifest from disk.
|
||||||
|
|
||||||
|
Raises ``FileNotFoundError`` if the manifest does not exist.
|
||||||
|
"""
|
||||||
|
inst = cls(key, project_root)
|
||||||
|
path = inst.manifest_path
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration manifest at {path} contains invalid JSON"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration manifest at {path} must be a JSON object, "
|
||||||
|
f"got {type(data).__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
files = data.get("files", {})
|
||||||
|
if not isinstance(files, dict) or not all(
|
||||||
|
isinstance(k, str) and isinstance(v, str) for k, v in files.items()
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
f"Integration manifest 'files' at {path} must be a "
|
||||||
|
"mapping of string paths to string hashes"
|
||||||
|
)
|
||||||
|
|
||||||
|
inst.version = data.get("version", "")
|
||||||
|
inst._installed_at = data.get("installed_at", "")
|
||||||
|
inst._files = files
|
||||||
|
|
||||||
|
stored_key = data.get("integration", "")
|
||||||
|
if stored_key and stored_key != key:
|
||||||
|
raise ValueError(
|
||||||
|
f"Manifest at {path} belongs to integration {stored_key!r}, "
|
||||||
|
f"not {key!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return inst
|
||||||
21
src/specify_cli/integrations/opencode/__init__.py
Normal file
21
src/specify_cli/integrations/opencode/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""opencode integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class OpencodeIntegration(MarkdownIntegration):
|
||||||
|
key = "opencode"
|
||||||
|
config = {
|
||||||
|
"name": "opencode",
|
||||||
|
"folder": ".opencode/",
|
||||||
|
"commands_subdir": "command",
|
||||||
|
"install_url": "https://opencode.ai",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".opencode/command",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — opencode integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType opencode
|
||||||
28
src/specify_cli/integrations/opencode/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/opencode/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — opencode integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" opencode
|
||||||
21
src/specify_cli/integrations/pi/__init__.py
Normal file
21
src/specify_cli/integrations/pi/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Pi Coding Agent integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class PiIntegration(MarkdownIntegration):
|
||||||
|
key = "pi"
|
||||||
|
config = {
|
||||||
|
"name": "Pi Coding Agent",
|
||||||
|
"folder": ".pi/",
|
||||||
|
"commands_subdir": "prompts",
|
||||||
|
"install_url": "https://www.npmjs.com/package/@mariozechner/pi-coding-agent",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".pi/prompts",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "AGENTS.md"
|
||||||
23
src/specify_cli/integrations/pi/scripts/update-context.ps1
Normal file
23
src/specify_cli/integrations/pi/scripts/update-context.ps1
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Pi Coding Agent integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType pi
|
||||||
28
src/specify_cli/integrations/pi/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/pi/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Pi Coding Agent integration: create/update AGENTS.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" pi
|
||||||
21
src/specify_cli/integrations/qodercli/__init__.py
Normal file
21
src/specify_cli/integrations/qodercli/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Qoder CLI integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class QodercliIntegration(MarkdownIntegration):
|
||||||
|
key = "qodercli"
|
||||||
|
config = {
|
||||||
|
"name": "Qoder CLI",
|
||||||
|
"folder": ".qoder/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://qoder.com/cli",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".qoder/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "QODER.md"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Qoder CLI integration: create/update QODER.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qodercli
|
||||||
28
src/specify_cli/integrations/qodercli/scripts/update-context.sh
Executable file
28
src/specify_cli/integrations/qodercli/scripts/update-context.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# update-context.sh — Qoder CLI integration: create/update QODER.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_root="$_script_dir"
|
||||||
|
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
||||||
|
if [ -z "${REPO_ROOT:-}" ]; then
|
||||||
|
if [ -d "$_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
else
|
||||||
|
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
||||||
|
REPO_ROOT="$git_root"
|
||||||
|
else
|
||||||
|
REPO_ROOT="$_root"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qodercli
|
||||||
21
src/specify_cli/integrations/qwen/__init__.py
Normal file
21
src/specify_cli/integrations/qwen/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Qwen Code integration."""
|
||||||
|
|
||||||
|
from ..base import MarkdownIntegration
|
||||||
|
|
||||||
|
|
||||||
|
class QwenIntegration(MarkdownIntegration):
|
||||||
|
key = "qwen"
|
||||||
|
config = {
|
||||||
|
"name": "Qwen Code",
|
||||||
|
"folder": ".qwen/",
|
||||||
|
"commands_subdir": "commands",
|
||||||
|
"install_url": "https://github.com/QwenLM/qwen-code",
|
||||||
|
"requires_cli": True,
|
||||||
|
}
|
||||||
|
registrar_config = {
|
||||||
|
"dir": ".qwen/commands",
|
||||||
|
"format": "markdown",
|
||||||
|
"args": "$ARGUMENTS",
|
||||||
|
"extension": ".md",
|
||||||
|
}
|
||||||
|
context_file = "QWEN.md"
|
||||||
23
src/specify_cli/integrations/qwen/scripts/update-context.ps1
Normal file
23
src/specify_cli/integrations/qwen/scripts/update-context.ps1
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# update-context.ps1 — Qwen Code integration: create/update QWEN.md
|
||||||
|
#
|
||||||
|
# Thin wrapper that delegates to the shared update-agent-context script.
|
||||||
|
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
||||||
|
#
|
||||||
|
# Until then, this delegates to the shared script as a subprocess.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Derive repo root from script location (walks up to find .specify/)
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
||||||
|
# If git did not return a repo root, or the git root does not contain .specify,
|
||||||
|
# fall back to walking up from the script directory to find the initialized project root.
|
||||||
|
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = $scriptDir
|
||||||
|
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
||||||
|
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
||||||
|
$repoRoot = Split-Path -Parent $repoRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qwen
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user