mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-10 09:41:17 +08:00
Merge branch 'main' into sandipkumar-yadav/skills
This commit is contained in:
commit
6b2ff7765e
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: Validate PR Title
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-title:
|
||||||
|
uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main
|
||||||
29
.github/workflows/validate-skills.yml
vendored
Normal file
29
.github/workflows/validate-skills.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Validate Skills
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-skills:
|
||||||
|
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
|
||||||
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 1.1.0 (2026-03-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add first pass at b2e sample ([ce615bf](https://github.com/forcedotcom/afv-library/commit/ce615bf0361e4a6a8f5635726ce304e6de921413))
|
||||||
|
* add metadata type skills ([3689f14](https://github.com/forcedotcom/afv-library/commit/3689f14af7eeb1dd8ea80ae206437f3483d37938))
|
||||||
|
* add skills from 260 ([65947b1](https://github.com/forcedotcom/afv-library/commit/65947b15509fef576ac683226976ee3d60a624f3))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "afv-library",
|
"name": "@salesforce/afv-skills",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "CC-BY-NC-4.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "*",
|
"@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": {
|
"node_modules/@salesforce/webapp-template-app-react-sample-b2e-experimental": {
|
||||||
@ -22,6 +468,106 @@
|
|||||||
"integrity": "sha512-ry4U36CjJx9h7pv5R5q0tVQCHkEvEYm/4u/5Ysh3UqTz89s3B9o/GPb0JK9fZ2Mgt/DCd0zfPUm1oRlCplnhGQ==",
|
"integrity": "sha512-ry4U36CjJx9h7pv5R5q0tVQCHkEvEYm/4u/5Ysh3UqTz89s3B9o/GPb0JK9fZ2Mgt/DCd0zfPUm1oRlCplnhGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "SEE LICENSE IN LICENSE.txt"
|
"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",
|
"name": "@salesforce/afv-skills",
|
||||||
"description": "AI prompts and rules library for Agentforce Vibes development",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"description": "Salesforce skills for Agentforce Vibes",
|
||||||
|
"license": "CC-BY-NC-4.0",
|
||||||
|
"files": [
|
||||||
|
"skills/"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"tsx": "^4.21.0",
|
||||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "*",
|
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "*",
|
||||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "*"
|
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"validate:skills": "tsx scripts/validate-skills.ts",
|
||||||
"sync-react-b2e-sample": "node scripts/sync-react-b2e-sample.js",
|
"sync-react-b2e-sample": "node scripts/sync-react-b2e-sample.js",
|
||||||
"sync-react-b2x-sample": "node scripts/sync-react-b2x-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()
|
||||||
171
skills/generate-permission-set/SKILL.md
Normal file
171
skills/generate-permission-set/SKILL.md
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
---
|
||||||
|
name: generate-permission-set
|
||||||
|
description: Generates correct, deployable Salesforce permission set metadata (PermissionSet XML) with object, field, user, and app permissions. Use when creating or editing permission set metadata, PermissionSet XML, object permissions, field-level security (FLS), tab visibility, or deploying permission sets.
|
||||||
|
compatibility: Salesforce Metadata API v60.0+
|
||||||
|
metadata:
|
||||||
|
author: afv-library
|
||||||
|
version: "1.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use when generating or editing permission set metadata, or when granting object, field, user, and app permissions.
|
||||||
|
|
||||||
|
## Step 1: Define Core Properties
|
||||||
|
|
||||||
|
Start by defining the required permission set properties:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
|
||||||
|
<fullName>YourPermissionSetName</fullName>
|
||||||
|
<label>Display Name for Administrators</label>
|
||||||
|
<description>Clear description of purpose and intended audience</description>
|
||||||
|
</PermissionSet>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Naming conventions:**
|
||||||
|
- Use descriptive API names (e.g., `Sales_Manager_Access`)
|
||||||
|
|
||||||
|
## Step 2: Configure Object Permissions
|
||||||
|
|
||||||
|
Add CRUD permissions for standard and custom objects:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<objectPermissions>
|
||||||
|
<allowCreate>true</allowCreate>
|
||||||
|
<allowRead>true</allowRead>
|
||||||
|
<allowEdit>true</allowEdit>
|
||||||
|
<allowDelete>false</allowDelete>
|
||||||
|
<modifyAllRecords>false</modifyAllRecords>
|
||||||
|
<viewAllRecords>false</viewAllRecords>
|
||||||
|
<viewAllFields>false</viewAllFields>
|
||||||
|
<object>Account</object>
|
||||||
|
</objectPermissions>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Set Field-Level Security
|
||||||
|
|
||||||
|
Define field permissions for sensitive or custom fields:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<fieldPermissions>
|
||||||
|
<editable>true</editable>
|
||||||
|
<readable>true</readable>
|
||||||
|
<field>Account.SSN__c</field>
|
||||||
|
</fieldPermissions>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
- Required fields must NEVER appear in list of field permissions. Granting field-level security on required fields is not allowed by the platform and will cause deployment failure.
|
||||||
|
- Before adding any field, confirm from the object metadata that the field exists and is not required
|
||||||
|
- A field is required when its metadata contains `<required>true</required>`:
|
||||||
|
```xml
|
||||||
|
<fields>
|
||||||
|
<fullName>FieldName__c</fullName>
|
||||||
|
<required>true</required>
|
||||||
|
</fields>
|
||||||
|
```
|
||||||
|
- Use format `ObjectName.FieldName` for field references
|
||||||
|
- Set both readable and editable to true when the user needs edit access; editable implies readable
|
||||||
|
- If all fields should be visible, can alternatively enable the "viewAllFields" object permission
|
||||||
|
|
||||||
|
## Step 4: Grant User Permissions
|
||||||
|
|
||||||
|
Add system-level permissions for features and capabilities:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<userPermissions>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
<name>ApiEnabled</name>
|
||||||
|
</userPermissions>
|
||||||
|
<userPermissions>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
<name>RunReports</name>
|
||||||
|
</userPermissions>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common permissions:**
|
||||||
|
- `ApiEnabled`: API access
|
||||||
|
- `ViewSetup`: View Setup menu
|
||||||
|
- `ManageUsers`: User management
|
||||||
|
- `RunReports`: Report execution
|
||||||
|
|
||||||
|
**Security review required for:**
|
||||||
|
- `ViewAllData`: Read all records
|
||||||
|
- `ModifyAllData`: Edit all records
|
||||||
|
- `ManageUsers`: User administration
|
||||||
|
|
||||||
|
## Step 5: Configure App and Tab Visibility
|
||||||
|
|
||||||
|
Make applications and tabs visible to users:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<applicationVisibilities>
|
||||||
|
<application>Sales_Console</application>
|
||||||
|
<visible>true</visible>
|
||||||
|
</applicationVisibilities>
|
||||||
|
<tabSettings>
|
||||||
|
<tab>CustomTab__c</tab>
|
||||||
|
<visibility>Visible</visibility>
|
||||||
|
</tabSettings>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Application visibility options:**
|
||||||
|
- <visible> can be true or false
|
||||||
|
|
||||||
|
**Tab visibility options:**
|
||||||
|
- `Visible`: Always shown
|
||||||
|
- `Available`: Available but not default
|
||||||
|
- `Hidden`: Not visible
|
||||||
|
|
||||||
|
**CRITICAL - Tab Naming:**
|
||||||
|
- Custom object tabs: MUST include the __c suffix (e.g., MyCustomObject__c)
|
||||||
|
- Standard object tabs: Use the object name with "standard-" prefix (e.g., standard-Account, standard-Contact)
|
||||||
|
- The tab name matches the object's API name exactly
|
||||||
|
|
||||||
|
## Step 6: Add Apex and Visualforce Access (Optional)
|
||||||
|
|
||||||
|
Grant access to custom code:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<classAccesses>
|
||||||
|
<apexClass>CustomController</apexClass>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</classAccesses>
|
||||||
|
<pageAccesses>
|
||||||
|
<apexPage>CustomPage</apexPage>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</pageAccesses>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Set License and Record Type Settings (Optional)
|
||||||
|
|
||||||
|
Specify license requirements and record type visibility:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<license>Salesforce</license>
|
||||||
|
<hasActivationRequired>false</hasActivationRequired>
|
||||||
|
<recordTypeVisibilities>
|
||||||
|
<recordType>Account.Business</recordType>
|
||||||
|
<visible>true</visible>
|
||||||
|
<default>true</default>
|
||||||
|
</recordTypeVisibilities>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Checklist
|
||||||
|
|
||||||
|
Before deploying, verify:
|
||||||
|
- [ ] fullName, label, description set
|
||||||
|
- [ ] Permissions follow least privilege
|
||||||
|
- [ ] No required fields in `<fieldPermissions>`
|
||||||
|
- [ ] No duplicate permissions
|
||||||
|
- [ ] no lengthy comments
|
||||||
|
|
||||||
|
## What Causes Deployment Failure
|
||||||
|
|
||||||
|
- **Field permissions on required fields:** Any required field in `<fieldPermissions>` fails deployment. Required fields cannot have FLS; omit them entirely. Always confirm from object/field metadata that a field exists and is not required—never assume.
|
||||||
|
- **Incorrect API names:** Using the wrong name or missing suffixes (e.g. missing `__c` for custom objects, fields, tabs) cause failure.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Deploy using Salesforce CLI
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: salesforce-flow
|
name: salesforce-flow
|
||||||
description: Generate Salesforce Flows using the MCP tool execute_metadata_action. Use when the user asks to create, build, or generate a flow — including Screen, Autolaunched, Record-Triggered (before/after-save), Scheduled, or Platform Event-Triggered Flows. Also trigger for flow-like requests such as "when a record is created", "trigger daily at", "send an email when", "update the field when", "automate", "workflow", or "flow XML/metadata". This is the only skill for Salesforce Flow generation.
|
description: Generate Salesforce Flows using the MCP tool execute_metadata_action. Use when the user asks to create, build, or generate a flow — including Screen, Autolaunched, Record-Triggered (before/after-save), Scheduled. Also trigger for flow-like requests such as "when a record is created", "trigger daily at", "send an email when", "update the field when", "automate", "workflow", or "flow XML/metadata". This is the only skill for Salesforce Flow generation.
|
||||||
---
|
---
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
@ -21,7 +21,7 @@ Use this skill when you need to:
|
|||||||
# Flow Metadata Specification
|
# Flow Metadata Specification
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
Salesforce Flows are powerful automation tools that enable complex business process automation without code. Flows can collect and process data through interactive screens, execute logic and calculations, manipulate records, call external services, and trigger based on various events. Flow types include Screen Flows (user-guided), Autolaunched Flows (background processing), Record-Triggered Flows (database events), Scheduled Flows (time-based), and Platform Event-Triggered Flows (event-driven).
|
Salesforce Flows are powerful automation tools that enable complex business process automation without code. Flows can collect and process data through interactive screens, execute logic and calculations, manipulate records, call external services, and trigger based on various events. Flow types include Screen Flows (user-guided), Autolaunched Flows (background processing), Record-Triggered Flows (database events) and Scheduled Flows (time-based).
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
- Automate complex business processes with declarative logic and branching
|
- Automate complex business processes with declarative logic and branching
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user