mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
ci: publish skills as @salesforce/afv-skills npm package @W-21534193@ (#30)
* ci: update package details and add GitHub workflows for skills validation and release * chore: update Node version, change license, and refactor skills validation script to TypeScript * chore: remove unused deps * fix: npm package * fix: flatten skills to pass validation * chore: add validation script to package.json and update GitHub workflow to use it * chore: enhance skills validation and update workflows for npm publishing * refactor: streamline skills validation process in workflow and script * refactor: improve documentation and structure of skills validation script * implement checks based on jeff's best practices doc * chore: add pull request template for skill submissions * chore: update pull request template with additional references and improve automated checks section * chore: update pull request template to clarify naming convention with a warning for gerund form * chore: update pull request template to reference skill authoring guide and enhance checklist structure * chore: enhance GitHub workflows for skills validation and release process * refactor: improve parsing logic for SKILL.md files and enhance error collection in validation * chore: update validation checks to enforce character limits for skill names and descriptions * fix: remove unnecessary whitespace in footer string in validate-skills.ts
This commit is contained in:
parent
eb5662a707
commit
2320c50264
43
.github/pull_request_template.md
vendored
Normal file
43
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
**References:** [Contributing guide](../CONTRIBUTING.md) · [Skill authoring guide](../README.md) · [Agent Skills spec](https://agentskills.io/specification)
|
||||
|
||||
## What changed
|
||||
|
||||
<!-- Briefly describe what skill(s) were added, updated, or removed. -->
|
||||
|
||||
## Why
|
||||
|
||||
<!-- What gap does this fill, or what problem does it solve? -->
|
||||
|
||||
## Notes
|
||||
|
||||
<!-- Anything reviewers should know — testing approach, follow-ups, open questions. -->
|
||||
|
||||
---
|
||||
|
||||
## Skills
|
||||
|
||||
### Manual checklist
|
||||
|
||||
**Description quality**
|
||||
- [ ] Describes what the skill does and the expected output
|
||||
- [ ] Includes relevant Salesforce domain keywords (Apex, LWC, SOQL, metadata types, etc.)
|
||||
- [ ] Trigger phrases are specific enough for Vibes to select this skill reliably
|
||||
|
||||
**Instructions**
|
||||
- [ ] Clear goal statement
|
||||
- [ ] Step-by-step workflow
|
||||
- [ ] Validation rules for generated output
|
||||
- [ ] Defined output / artifact
|
||||
|
||||
**Context efficiency**
|
||||
- [ ] Core instructions are concise — supporting material lives in `templates/`, `examples/`, or `docs/` subdirectories
|
||||
- [ ] No unnecessary background explanation in the body
|
||||
|
||||
### Automated checks
|
||||
|
||||
Enforced by CI ([`npm run validate:skills`](../scripts/validate-skills.ts)) per the [Agent Skills spec](https://agentskills.io/specification):
|
||||
|
||||
- Directory is one level deep, named in kebab-case (max 64 chars), contains `SKILL.md`
|
||||
- Frontmatter `name` matches directory name; `description` is present, ≥ 20 words, ≤ 1024 characters, and includes trigger language
|
||||
- Body is non-empty and under 500 lines
|
||||
- Name uses gerund form ⚠ (warning — does not block merge)
|
||||
91
.github/workflows/release-skills.yml
vendored
Normal file
91
.github/workflows/release-skills.yml
vendored
Normal file
@ -0,0 +1,91 @@
|
||||
name: release-skills
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['skills/**']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Branch or tag to release from (must be main)"
|
||||
required: false
|
||||
default: main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skipped: ${{ steps.changelog.outputs.skipped }}
|
||||
version: ${{ steps.changelog.outputs.version }}
|
||||
steps:
|
||||
- name: Require main branch
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
if [ "${{ github.ref_name }}" != "main" ]; then
|
||||
echo "Releases must be run from the main branch (got '${{ github.ref_name }}')."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Validate skills
|
||||
run: npm run validate:skills
|
||||
|
||||
- name: Conventional Changelog Action
|
||||
id: changelog
|
||||
uses: TriPSs/conventional-changelog-action@v5
|
||||
with:
|
||||
git-user-name: svc-idee-bot
|
||||
git-user-email: svc_idee_bot@salesforce.com
|
||||
github-token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
tag-prefix: ""
|
||||
release-count: "0"
|
||||
skip-on-empty: ${{ github.event_name == 'push' }}
|
||||
version-file: "package.json"
|
||||
output-file: "CHANGELOG.md"
|
||||
|
||||
- name: Create Github Release
|
||||
id: release
|
||||
uses: ncipollo/release-action@v1
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' }}
|
||||
with:
|
||||
name: "${{ steps.changelog.outputs.version }}"
|
||||
tag: "${{ steps.changelog.outputs.version }}"
|
||||
commit: ${{ github.sha }}
|
||||
body: |
|
||||
## Changes in @salesforce/afv-skills
|
||||
|
||||
${{ steps.changelog.outputs.clean_changelog }}
|
||||
token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
skipIfReleaseExists: true
|
||||
|
||||
- name: Publish to npm
|
||||
id: publish
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' && steps.release.outputs.id != '' }}
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
npm publish --access public
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Notify cline-fork
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' && steps.release.outputs.id != '' && steps.publish.outcome == 'success' }}
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
repository: forcedotcom/cline-fork
|
||||
event-type: skills-released
|
||||
client-payload: '{"version": "${{ steps.changelog.outputs.version }}"}'
|
||||
10
.github/workflows/validate-pr.yml
vendored
Normal file
10
.github/workflows/validate-pr.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: pr-validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, edited]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
pr-validation:
|
||||
uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main
|
||||
36
.github/workflows/validate-skills.yml
vendored
Normal file
36
.github/workflows/validate-skills.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Validate Skills
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'skills/**'
|
||||
- 'scripts/validate-skills.ts'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.nvmrc'
|
||||
- '.github/workflows/validate-skills.yml'
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: npm ci
|
||||
|
||||
# Run against only changed skills when skills files are touched; fall back
|
||||
# to full corpus validation when only tooling files changed, so validator
|
||||
# regressions against existing skills are caught in the same PR.
|
||||
- name: Validate skills
|
||||
run: |
|
||||
if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q '^skills/'; then
|
||||
npm run validate:skills -- --changed --base=origin/${{ github.base_ref }}
|
||||
else
|
||||
npm run validate:skills
|
||||
fi
|
||||
552
package-lock.json
generated
552
package-lock.json
generated
@ -1,13 +1,459 @@
|
||||
{
|
||||
"name": "afv-library",
|
||||
"name": "@salesforce/afv-skills",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "afv-library",
|
||||
"name": "@salesforce/afv-skills",
|
||||
"version": "1.0.0",
|
||||
"license": "CC-BY-NC-4.0",
|
||||
"devDependencies": {
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "*",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "*"
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "*",
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@salesforce/webapp-template-app-react-sample-b2e-experimental": {
|
||||
@ -22,6 +468,106 @@
|
||||
"integrity": "sha512-ry4U36CjJx9h7pv5R5q0tVQCHkEvEYm/4u/5Ysh3UqTz89s3B9o/GPb0JK9fZ2Mgt/DCd0zfPUm1oRlCplnhGQ==",
|
||||
"dev": true,
|
||||
"license": "SEE LICENSE IN LICENSE.txt"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.6",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
package.json
16
package.json
@ -1,12 +1,22 @@
|
||||
{
|
||||
"name": "afv-library",
|
||||
"description": "AI prompts and rules library for Agentforce Vibes development",
|
||||
"private": true,
|
||||
"name": "@salesforce/afv-skills",
|
||||
"version": "1.0.0",
|
||||
"description": "Salesforce skills for Agentforce Vibes",
|
||||
"license": "CC-BY-NC-4.0",
|
||||
"files": [
|
||||
"skills/"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.21.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "*",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"validate:skills": "tsx scripts/validate-skills.ts",
|
||||
"sync-react-b2e-sample": "node scripts/sync-react-b2e-sample.js",
|
||||
"sync-react-b2x-sample": "node scripts/sync-react-b2x-sample.js"
|
||||
}
|
||||
|
||||
368
scripts/validate-skills.ts
Normal file
368
scripts/validate-skills.ts
Normal file
@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env tsx
|
||||
// Validates the skills/ directory structure and SKILL.md format.
|
||||
// Exits with code 1 if any violations are found.
|
||||
//
|
||||
// Usage:
|
||||
// npm run validate:skills # validate all skills
|
||||
// npm run validate:skills -- --changed --base=origin/main # validate only skills changed vs base
|
||||
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { parseArgs } from "util"
|
||||
|
||||
const SKILLS_DIR = path.join(__dirname, "..", "skills")
|
||||
|
||||
/** Parsed context for a single skill directory, built before content checks run. */
|
||||
interface SkillContext {
|
||||
dirName: string
|
||||
dirPath: string
|
||||
content: string
|
||||
frontmatter: Record<string, string> | null
|
||||
body: string
|
||||
}
|
||||
|
||||
/** Return value of every check function. */
|
||||
interface CheckResult {
|
||||
errors: string[]
|
||||
/** When true and errors is non-empty, skip remaining checks for this entry. */
|
||||
fatal?: boolean
|
||||
/** Defaults to "error". Warnings are printed but do not cause a non-zero exit code. */
|
||||
severity?: "error" | "warning"
|
||||
}
|
||||
|
||||
/** Collected results for a single skill entry. */
|
||||
interface SkillResult {
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
/** Runs before SKILL.md is read — validates directory layout. */
|
||||
interface StructureCheck {
|
||||
description: string
|
||||
run(dirName: string, dirPath: string): CheckResult
|
||||
}
|
||||
|
||||
/** Runs after SKILL.md is read — validates file content. */
|
||||
interface ContentCheck {
|
||||
description: string
|
||||
run(ctx: SkillContext): CheckResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Structure checks run on every entry in `skills/` before SKILL.md is read.
|
||||
* A fatal error aborts content checks for that entry.
|
||||
*/
|
||||
const STRUCTURE_CHECKS: StructureCheck[] = [
|
||||
{
|
||||
description: "Entry must be a directory (no loose files in skills/)",
|
||||
run(dirName, dirPath) {
|
||||
if (!fs.statSync(dirPath).isDirectory()) {
|
||||
return { errors: [`Loose file in skills/: ${dirName} (expected only directories)`], fatal: true }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skill directory must contain SKILL.md",
|
||||
run(dirName, dirPath) {
|
||||
if (!fs.existsSync(path.join(dirPath, "SKILL.md"))) {
|
||||
return { errors: [`Missing SKILL.md in skills/${dirName}/`], fatal: true }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Name must be kebab-case (lowercase letters, digits, and hyphens only)",
|
||||
run(dirName) {
|
||||
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(dirName)) {
|
||||
return { errors: [`skills/${dirName}: name must be kebab-case (only lowercase letters, digits, and hyphens)`] }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Name must be at most 64 characters",
|
||||
run(dirName) {
|
||||
if (dirName.length > 64) {
|
||||
return { errors: [`skills/${dirName}: name is ${dirName.length} characters (maximum 64)`] }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Name should use gerund form — first word should end in -ing (e.g. generating-apex-tests)",
|
||||
run(dirName) {
|
||||
if (!dirName.split("-")[0].endsWith("ing")) {
|
||||
return {
|
||||
errors: [`skills/${dirName}: name should use gerund form (e.g. generating-apex-tests, refactoring-triggers)`],
|
||||
severity: "warning",
|
||||
}
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skills must be exactly one level deep (no nested category directories)",
|
||||
run(dirName, dirPath) {
|
||||
const errors: string[] = []
|
||||
for (const sub of fs.readdirSync(dirPath)) {
|
||||
const subPath = path.join(dirPath, sub)
|
||||
if (fs.statSync(subPath).isDirectory() && fs.existsSync(path.join(subPath, "SKILL.md"))) {
|
||||
errors.push(
|
||||
`Nested skill detected: skills/${dirName}/${sub}/SKILL.md — skill directories must be exactly one level deep under skills/`
|
||||
)
|
||||
}
|
||||
}
|
||||
return { errors }
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Content checks run only on entries that have a valid SKILL.md.
|
||||
* A fatal error aborts remaining content checks for that entry.
|
||||
*/
|
||||
const CONTENT_CHECKS: ContentCheck[] = [
|
||||
{
|
||||
description: "SKILL.md must have a valid YAML frontmatter block (--- ... ---)",
|
||||
run({ dirName, frontmatter }) {
|
||||
if (!frontmatter) {
|
||||
return {
|
||||
errors: [`skills/${dirName}/SKILL.md: missing or malformed YAML frontmatter (expected --- ... --- block at top)`],
|
||||
fatal: true,
|
||||
}
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Frontmatter "name" must be present and match the directory name',
|
||||
run({ dirName, frontmatter }) {
|
||||
if (!frontmatter) return { errors: [] }
|
||||
if (!frontmatter.name) {
|
||||
return { errors: [`skills/${dirName}/SKILL.md: missing "name" field in frontmatter`] }
|
||||
}
|
||||
if (frontmatter.name !== dirName) {
|
||||
return {
|
||||
errors: [
|
||||
`skills/${dirName}/SKILL.md: "name" value ("${frontmatter.name}") does not match directory name ("${dirName}")`,
|
||||
],
|
||||
}
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Frontmatter "description" must be present and non-empty',
|
||||
run({ dirName, frontmatter }) {
|
||||
if (!frontmatter) return { errors: [] }
|
||||
if (!frontmatter.description?.trim()) {
|
||||
return { errors: [`skills/${dirName}/SKILL.md: missing or empty "description" field in frontmatter`] }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "SKILL.md must have a non-empty body (instructions after the frontmatter block)",
|
||||
run({ dirName, body }) {
|
||||
if (!body.trim()) {
|
||||
return { errors: [`skills/${dirName}/SKILL.md: body (instructions after frontmatter) is empty`] }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Description must be at least 20 words to be information-rich",
|
||||
run({ dirName, frontmatter }) {
|
||||
if (!frontmatter) return { errors: [] }
|
||||
const words = frontmatter.description?.trim().split(/\s+/) ?? []
|
||||
if (words.length < 20) {
|
||||
return {
|
||||
errors: [`skills/${dirName}/SKILL.md: description too short (${words.length} word(s), minimum 20)`],
|
||||
}
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Description must be at most 1024 characters",
|
||||
run({ dirName, frontmatter }) {
|
||||
if (!frontmatter) return { errors: [] }
|
||||
const len = frontmatter.description?.length ?? 0
|
||||
if (len > 1024) {
|
||||
return { errors: [`skills/${dirName}/SKILL.md: description is ${len} characters (maximum 1024)`] }
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Description must include trigger/activation language (contain "use")',
|
||||
run({ dirName, frontmatter }) {
|
||||
if (!frontmatter) return { errors: [] }
|
||||
if (!frontmatter.description?.toLowerCase().includes("use")) {
|
||||
return {
|
||||
errors: [
|
||||
`skills/${dirName}/SKILL.md: description must include trigger context (e.g. "Use this skill when...")`,
|
||||
],
|
||||
}
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Skill body should be under 500 lines for context efficiency",
|
||||
run({ dirName, body }) {
|
||||
const lines = body.split("\n").length
|
||||
if (lines > 500) {
|
||||
return {
|
||||
errors: [`skills/${dirName}/SKILL.md: body is ${lines} lines (recommended maximum is 500)`],
|
||||
severity: "warning",
|
||||
}
|
||||
}
|
||||
return { errors: [] }
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Returns the deduplicated list of top-level skill directory names that have
|
||||
* changed relative to `base` (e.g. `origin/main`) and still exist on disk.
|
||||
* Deleted skill directories are intentionally excluded — removing a skill is
|
||||
* valid and requires no structural validation.
|
||||
*/
|
||||
function getChangedSkillDirs(base: string): string[] {
|
||||
const output = execSync(`git diff --name-only ${base}...HEAD`, { encoding: "utf8" })
|
||||
return [
|
||||
...new Set(
|
||||
output
|
||||
.split("\n")
|
||||
.filter((f) => f.startsWith("skills/"))
|
||||
.map((f) => f.split("/")[1])
|
||||
.filter(Boolean)
|
||||
),
|
||||
].filter((dir) => fs.existsSync(path.join(SKILLS_DIR, dir)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a SKILL.md file into its frontmatter fields and body.
|
||||
* `frontmatter` is `null` if the `--- ... ---` block is missing or malformed.
|
||||
* Wrapping quotes on frontmatter values are stripped.
|
||||
*/
|
||||
function parseSkillMd(content: string): { frontmatter: Record<string, string> | null; body: string } {
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/)
|
||||
if (!match) return { frontmatter: null, body: content }
|
||||
|
||||
const frontmatter: Record<string, string> = {}
|
||||
for (const line of match[1].split(/\r?\n/)) {
|
||||
const colonIdx = line.indexOf(":")
|
||||
if (colonIdx === -1) continue
|
||||
const key = line.slice(0, colonIdx).trim()
|
||||
const raw = line.slice(colonIdx + 1).trim()
|
||||
// Strip wrapping single or double quotes to match how consumers read values
|
||||
frontmatter[key] = raw.replace(/^(['"])([\s\S]*)\1$/, "$2")
|
||||
}
|
||||
|
||||
return { frontmatter, body: content.slice(match[0].length) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all structure and content checks for a single skill directory.
|
||||
* Returns errors (block CI) and warnings (advisory, printed but exit 0) separately.
|
||||
*/
|
||||
function validateSkill(dirName: string, dirPath: string): SkillResult {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return { errors: [`skills/${dirName}: directory not found`], warnings: [] }
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
const collectIssues = (result: CheckResult): boolean => {
|
||||
if (result.errors.length === 0) return false
|
||||
if (result.severity === "warning") {
|
||||
warnings.push(...result.errors)
|
||||
} else {
|
||||
errors.push(...result.errors)
|
||||
}
|
||||
return result.fatal === true && result.severity !== "warning"
|
||||
}
|
||||
|
||||
for (const check of STRUCTURE_CHECKS) {
|
||||
if (collectIssues(check.run(dirName, dirPath))) return { errors, warnings }
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(path.join(dirPath, "SKILL.md"), "utf8")
|
||||
const { frontmatter, body } = parseSkillMd(content)
|
||||
const ctx: SkillContext = { dirName, dirPath, content, frontmatter, body }
|
||||
|
||||
for (const check of CONTENT_CHECKS) {
|
||||
if (collectIssues(check.run(ctx))) return { errors, warnings }
|
||||
}
|
||||
|
||||
return { errors, warnings }
|
||||
}
|
||||
|
||||
/** CLI entry point. Parses flags, resolves the list of skills to check, and reports results. */
|
||||
function main(): void {
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
/** Validate only skill dirs touched in this branch vs the given base ref. */
|
||||
changed: { type: "boolean", default: false },
|
||||
/** Base ref for --changed (e.g. origin/main). Defaults to origin/HEAD. */
|
||||
base: { type: "string", default: "origin/HEAD" },
|
||||
},
|
||||
})
|
||||
|
||||
let entries: string[]
|
||||
|
||||
if (values.changed) {
|
||||
entries = getChangedSkillDirs(values.base!)
|
||||
if (entries.length === 0) {
|
||||
console.log("No skill directories changed — nothing to validate.")
|
||||
return
|
||||
}
|
||||
console.log(`Validating ${entries.length} changed skill(s): ${entries.join(", ")}`)
|
||||
} else {
|
||||
entries = fs.readdirSync(SKILLS_DIR)
|
||||
}
|
||||
|
||||
const allErrors: string[] = []
|
||||
const allWarnings: string[] = []
|
||||
let passed = 0
|
||||
|
||||
for (const entry of entries) {
|
||||
const { errors, warnings } = validateSkill(entry, path.join(SKILLS_DIR, entry))
|
||||
allErrors.push(...errors)
|
||||
allWarnings.push(...warnings)
|
||||
if (errors.length === 0) passed++
|
||||
}
|
||||
|
||||
const hasIssues = allErrors.length > 0 || allWarnings.length > 0
|
||||
const footer = "Spec: https://agentskills.io/specification · Authoring guide: https://github.com/forcedotcom/afv-library#readme"
|
||||
|
||||
if (allWarnings.length > 0) {
|
||||
console.warn(`\n${allWarnings.length} warning(s):\n`)
|
||||
for (const w of allWarnings) {
|
||||
console.warn(` ⚠ ${w}`)
|
||||
}
|
||||
console.warn("")
|
||||
}
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
console.error(`\nSkill validation failed with ${allErrors.length} error(s):\n`)
|
||||
for (const err of allErrors) {
|
||||
console.error(` ✗ ${err}`)
|
||||
}
|
||||
console.error("")
|
||||
console.error(footer)
|
||||
console.error("")
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log(`Skill validation passed: ${passed} of ${entries.length} skill(s) checked.`)
|
||||
if (hasIssues) console.warn(footer)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user