diff --git a/src/scaffold.ts b/src/scaffold.ts index fc88bf6..6acde99 100644 --- a/src/scaffold.ts +++ b/src/scaffold.ts @@ -4,8 +4,10 @@ import { mkdir, readdir, readFile, + readlink, rename, rm, + symlink, writeFile, } from "node:fs/promises"; import { join } from "node:path"; @@ -32,7 +34,10 @@ export async function copyDir(src: string, dest: string): Promise { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); - if (entry.isDirectory()) { + if (entry.isSymbolicLink()) { + const target = await readlink(srcPath); + await symlink(target, destPath); + } else if (entry.isDirectory()) { await copyDir(srcPath, destPath); } else { await cp(srcPath, destPath); @@ -70,7 +75,9 @@ export async function processDirectory( for (const entry of entries) { const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { + if (entry.isSymbolicLink()) { + // Symlinks are preserved as-is (target already copied) + } else if (entry.isDirectory()) { if (entry.name === SKILL_NAME_PLACEHOLDER) { const newPath = join(dir, values.Skill_Name); await rename(fullPath, newPath); diff --git a/template/.claude-plugin/marketplace.json b/template/.claude-plugin/marketplace.json index aa8aaa1..e873722 100644 --- a/template/.claude-plugin/marketplace.json +++ b/template/.claude-plugin/marketplace.json @@ -1,5 +1,5 @@ { - "name": "{Brand_Name}-skills", + "name": "{Brand_Name}-agent-skills", "description": "{Brand_Name} agent skills", "owner": { "name": "{Creator_Name}", @@ -7,18 +7,18 @@ }, "metadata": { "version": "1.0.0", - "description": "Claude Code marketplace of skills by {Brand_Name}" + "description": "Claude Code marketplace of agent skills by {Brand_Name}" }, "plugins": [ { - "name": "{Skill_Name}-skill", - "description": "{Skill_Description}", + "name": "{Brand_Name}-agent-skills", + "description": "{Brand_Name} agent skills", "version": "1.0.0", "author": { "name": "{Creator_Name}", "email": "{Creator_Email}" }, - "source": "./skills/{Skill_Name}", + "source": "./", "homepage": "{Skill_Homepage}", "repository": "https://github.com/{Skill_Repository}", "license": "{Skill_License}", diff --git a/template/.claude-plugin/plugin.json b/template/.claude-plugin/plugin.json new file mode 100644 index 0000000..baf071c --- /dev/null +++ b/template/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "{Brand_Name}-agent-skills", + "description": "{Brand_Name} agent skills", + "version": "1.0.0", + "author": { "name": "{Creator_Name}", "email": "{Creator_Email}" }, + "homepage": "{Skill_Homepage}", + "repository": "https://github.com/{Skill_Repository}", + "license": "{Skill_License}", + "keywords": "{Skill_Keywords}", + "skills": "./skills/" +} diff --git a/template/.claude-plugin/plugin.schema.json b/template/.claude-plugin/plugin.schema.json new file mode 100644 index 0000000..af05d34 --- /dev/null +++ b/template/.claude-plugin/plugin.schema.json @@ -0,0 +1,118 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "claude-plugin.schema.json", + "title": "Claude Code Plugin", + "description": "Schema for Claude Code plugin.json metadata", + "type": "object", + "required": ["name"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference" + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9.-]*$", + "description": "Unique plugin identifier. Lowercase, kebab-case (alphanumerics, hyphens, and periods)" + }, + "description": { + "type": "string", + "description": "Brief explanation of plugin purpose" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "description": "Semantic version (e.g., 1.0.0, 0.1.0-beta)" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Author name" + }, + "email": { + "type": "string", + "description": "Author email" + }, + "url": { + "type": "string", + "description": "Author URL" + } + }, + "required": ["name"] + }, + "homepage": { + "type": "string", + "description": "Documentation URL" + }, + "repository": { + "type": "string", + "description": "Source code repository URL" + }, + "license": { + "type": "string", + "description": "SPDX license identifier (e.g., MIT, Apache-2.0)" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Discovery tags for search" + }, + "commands": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Additional command files or directories" + }, + "agents": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Additional agent files or directories" + }, + "skills": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Additional skill directories" + }, + "hooks": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } }, + { "type": "object" } + ], + "description": "Hook config paths or inline configuration" + }, + "mcpServers": { + "oneOf": [ + { "type": "string" }, + { "type": "array" }, + { "type": "object" } + ], + "description": "MCP config paths or inline configuration" + }, + "lspServers": { + "oneOf": [ + { "type": "string" }, + { "type": "array" }, + { "type": "object" } + ], + "description": "LSP server config paths or inline configuration" + }, + "outputStyles": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Additional output style files or directories" + } + }, + "additionalProperties": false +} diff --git a/template/.cursor-plugin/plugin.json b/template/.cursor-plugin/plugin.json new file mode 100644 index 0000000..baf071c --- /dev/null +++ b/template/.cursor-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "{Brand_Name}-agent-skills", + "description": "{Brand_Name} agent skills", + "version": "1.0.0", + "author": { "name": "{Creator_Name}", "email": "{Creator_Email}" }, + "homepage": "{Skill_Homepage}", + "repository": "https://github.com/{Skill_Repository}", + "license": "{Skill_License}", + "keywords": "{Skill_Keywords}", + "skills": "./skills/" +} diff --git a/template/.cursor-plugin/plugin.schema.json b/template/.cursor-plugin/plugin.schema.json new file mode 100644 index 0000000..2fc31b9 --- /dev/null +++ b/template/.cursor-plugin/plugin.schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "cursor-plugin.schema.json", + "title": "Cursor Plugin", + "description": "Schema for Cursor plugin.json metadata", + "type": "object", + "required": ["name"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema reference" + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9.-]*$", + "description": "Plugin identifier. Lowercase, kebab-case (alphanumerics, hyphens, and periods)" + }, + "description": { + "type": "string", + "description": "Brief overview of plugin functionality" + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "description": "Semantic version (e.g., 1.0.0)" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Creator name" + }, + "email": { + "type": "string", + "description": "Creator email" + } + }, + "required": ["name"] + }, + "homepage": { + "type": "string", + "description": "Web address for plugin documentation" + }, + "repository": { + "type": "string", + "description": "Web address for plugin source code" + }, + "license": { + "type": "string", + "description": "License type identifier (e.g., MIT)" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Search tags for discoverability" + }, + "logo": { + "type": "string", + "description": "Relative file path or absolute URL to logo asset" + }, + "rules": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Path(s) to rule files or folders" + }, + "agents": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Path(s) to agent files or folders" + }, + "skills": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Path(s) to skill directories" + }, + "commands": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "description": "Path(s) to command files or folders" + }, + "hooks": { + "oneOf": [{ "type": "string" }, { "type": "object" }], + "description": "Path to hooks configuration or inline configuration" + }, + "mcpServers": { + "oneOf": [ + { "type": "string" }, + { "type": "object" }, + { "type": "array" } + ], + "description": "Path to MCP config, inline config, or array of either" + } + }, + "additionalProperties": false +} diff --git a/template/.github/workflows/process-skills.yml b/template/.github/workflows/process-skills.yml deleted file mode 100644 index d9a97f3..0000000 --- a/template/.github/workflows/process-skills.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Process Skills - -on: - push: - branches: [main] - paths: - - "**/SKILL.md" - pull_request: - branches: [main] - paths: - - "**/SKILL.md" - -concurrency: - group: skills-${{ github.ref }} - cancel-in-progress: true - -jobs: - detect-changes: - runs-on: ubuntu-slim - if: github.event.pull_request.draft != true && github.actor != 'dependabot[bot]' - outputs: - skills: ${{ steps.changed-skills.outputs.skills }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Get changed skills - id: changed-skills - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - BASE=${{ github.event.pull_request.base.sha }} - HEAD=${{ github.event.pull_request.head.sha }} - else - BASE=${{ github.event.before }} - HEAD=${{ github.event.after }} - # Handle initial commit or force push (BASE doesn't exist) - if [ "$BASE" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$BASE" 2>/dev/null; then - SKILLS=$(ls -d skills/*/ 2>/dev/null | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(length > 0))') - echo "skills=$SKILLS" >> $GITHUB_OUTPUT - echo "Initial/force push - all skills: $SKILLS" - exit 0 - fi - fi - - # Find changed files in skills/ and extract skill directories - SKILLS=$(git diff --name-only $BASE $HEAD | \ - grep '^skills/' | \ - sed 's|skills/\([^/]*\)/.*|\1|' | \ - sort -u | \ - jq -R -s -c 'split("\n") | map(select(length > 0))') - - echo "skills=$SKILLS" >> $GITHUB_OUTPUT - echo "Changed skills: $SKILLS" - - validate: - needs: detect-changes - if: needs.detect-changes.outputs.skills != '[]' - runs-on: ubuntu-slim - strategy: - fail-fast: false - matrix: - skill: ${{ fromJson(needs.detect-changes.outputs.skills) }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Validate ${{ matrix.skill }} - uses: Flash-Brew-Digital/validate-skill@v1 - with: - path: skills/${{ matrix.skill }} - - sync: - needs: validate - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-slim - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - persist-credentials: true - - - name: Sync skills - run: node scripts/sync-skills.js - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v7 - with: - commit_message: "chore: sync skills documentation" - commit_user_name: "Agent Skills Bot" - commit_user_email: "agent-skills-bot@users.noreply.github.com" - file_pattern: "README.md .claude-plugin/marketplace.json skills/*/.claude-plugin/plugin.json" \ No newline at end of file diff --git a/template/.github/workflows/validate-and-sync.yml b/template/.github/workflows/validate-and-sync.yml new file mode 100644 index 0000000..1b84e41 --- /dev/null +++ b/template/.github/workflows/validate-and-sync.yml @@ -0,0 +1,162 @@ +name: Validate & Sync Skills and Plugins + +on: + push: + branches: [main] + paths: + - "skills/**" + - "manifest.json" + - ".claude-plugin/**" + - ".cursor-plugin/**" + pull_request: + branches: [main] + paths: + - "skills/**" + - "manifest.json" + - ".claude-plugin/**" + - ".cursor-plugin/**" + +concurrency: + group: skills-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect-changes: + runs-on: ubuntu-slim + if: github.event.pull_request.draft != true && github.actor != 'dependabot[bot]' + outputs: + skills: ${{ steps.changed-skills.outputs.skills }} + skills_to_validate: ${{ steps.changed-skills.outputs.skills_to_validate }} + plugins_changed: ${{ steps.changed-plugins.outputs.changed }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get changed skills + id: changed-skills + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE=${{ github.event.pull_request.base.sha }} + HEAD=${{ github.event.pull_request.head.sha }} + else + BASE=${{ github.event.before }} + HEAD=${{ github.event.after }} + # Handle initial commit or force push (BASE doesn't exist) + if [ "$BASE" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$BASE" 2>/dev/null; then + SKILLS=$(ls -d skills/*/ 2>/dev/null | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(length > 0))') + echo "skills=$SKILLS" >> $GITHUB_OUTPUT + echo "skills_to_validate=$SKILLS" >> $GITHUB_OUTPUT + echo "Initial/force push - all skills: $SKILLS" + exit 0 + fi + fi + + CHANGED_FILES=$(git diff --name-only $BASE $HEAD) + + # All changed skills (for packaging) + SKILLS=$(echo "$CHANGED_FILES" | \ + grep '^skills/' | \ + sed 's|skills/\([^/]*\)/.*|\1|' | \ + sort -u | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + # Skills with SKILL.md changes (for validation) + SKILLS_TO_VALIDATE=$(echo "$CHANGED_FILES" | \ + grep '^skills/.*SKILL\.md$' | \ + sed 's|skills/\([^/]*\)/.*|\1|' | \ + sort -u | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "skills=$SKILLS" >> $GITHUB_OUTPUT + echo "skills_to_validate=$SKILLS_TO_VALIDATE" >> $GITHUB_OUTPUT + echo "Changed skills: $SKILLS" + echo "Skills to validate: $SKILLS_TO_VALIDATE" + + - name: Check for plugin changes + id: changed-plugins + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE=${{ github.event.pull_request.base.sha }} + HEAD=${{ github.event.pull_request.head.sha }} + else + BASE=${{ github.event.before }} + HEAD=${{ github.event.after }} + if [ "$BASE" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$BASE" 2>/dev/null; then + echo "changed=true" >> $GITHUB_OUTPUT + exit 0 + fi + fi + + CHANGED=$(git diff --name-only $BASE $HEAD | grep -E '^(manifest\.json|\.claude-plugin/|\.cursor-plugin/)' | wc -l) + if [ "$CHANGED" -gt 0 ]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + validate-skills: + needs: detect-changes + if: needs.detect-changes.outputs.skills_to_validate != '[]' + runs-on: ubuntu-slim + strategy: + fail-fast: false + matrix: + skill: ${{ fromJson(needs.detect-changes.outputs.skills_to_validate) }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Check skill exists + id: check + run: | + if [ -d "skills/${{ matrix.skill }}" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Skill '${{ matrix.skill }}' was deleted, skipping validation" + fi + + - name: Validate ${{ matrix.skill }} + if: steps.check.outputs.exists == 'true' + uses: Flash-Brew-Digital/validate-skill@v1 + with: + path: skills/${{ matrix.skill }} + + validate-plugins: + needs: detect-changes + if: needs.detect-changes.outputs.plugins_changed == 'true' + runs-on: ubuntu-slim + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Validate plugins + run: | + npx ajv-cli validate -s .claude-plugin/plugin.schema.json -d .claude-plugin/plugin.json + npx ajv-cli validate -s .cursor-plugin/plugin.schema.json -d .cursor-plugin/plugin.json + + sync: + needs: [validate-skills, validate-plugins] + if: always() && !failure() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-slim + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + persist-credentials: true + + - name: Sync skills + run: node scripts/sync-skills.js + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "chore: sync skills documentation [skip ci]" + commit_user_name: "Agent Skills Bot" + commit_user_email: "agent-skills-bot@users.noreply.github.com" + file_pattern: "manifest.json skills/index.json README.md .claude-plugin/plugin.json .claude-plugin/marketplace.json .cursor-plugin/plugin.json" diff --git a/template/AGENTS.md b/template/AGENTS.md new file mode 100644 index 0000000..b9d019f --- /dev/null +++ b/template/AGENTS.md @@ -0,0 +1,98 @@ +# {Brand_Name} - Agent Skills Repository +## AGENTS.md (Symlink to CLAUDE.md) + +This repo contains {Brand_Name} Agent Skills — folders of instructions, references, scripts, and assets that AI agents use to work more accurately. For detailed architecture, see [ARCHITECTURE.md](ARCHITECTURE.md). + +## Repo Structure + +``` +manifest.json # Root source of truth — global config + skills array +skills/ +├── index.json # Generated — agent-skills-discovery RFC index +└── / + ├── SKILL.md # Entry point — frontmatter + overview + reference index + ├── references/ # Detailed reference docs (API guides, guidelines, etc.) + ├── scripts/ # Helper scripts for the skill + └── assets/ # Static assets (CSS, images, etc.) +``` + +## Reference File Conventions + +Every file in `references/` must have YAML frontmatter with three fields: + +```yaml +--- +name: "Human-Readable Title" +description: "One-line summary of the file's contents." +tags: [tag1, tag2, tag3] +--- +``` + +- **name**: Descriptive title (e.g., "Elements API", "Marketplace Guidelines") +- **description**: Single sentence summarizing what the file covers +- **tags**: Array of searchable keywords — include API method names, category terms, and key concepts + +### Content style + +- Plain markdown only — no JSX components (``, ``, ``, ``, etc.) +- Use fenced code blocks with language identifiers (e.g., ` ```typescript `) +- Use markdown tables for structured data +- Use blockquotes (`>`) for callouts and notes +- End each reference with a "Best Practices" section where applicable +- Keep references focused on one API domain or topic per file + +### When adding references from external docs + +Source documentation often uses JSX/HTML components. Strip these when converting: + +| Source component | Convert to | +|---|---| +| `` / `` | Separate sections with `###` headings | +| `` / `` | Blockquote (`>`) | +| `` / `` | Numbered list with `###` sub-headings | +| `` | Standard markdown table or section | +| `` / `` | Remove (image URLs won't resolve in this context) | +| `