diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..bc19414 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,43 @@ +**References:** [Contributing guide](../CONTRIBUTING.md) · [Skill authoring guide](../README.md) · [Agent Skills spec](https://agentskills.io/specification) + +## What changed + + + +## Why + + + +## Notes + + + +--- + +## 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) diff --git a/.github/workflows/release-skills.yml b/.github/workflows/release-skills.yml new file mode 100644 index 0000000..3cd55bd --- /dev/null +++ b/.github/workflows/release-skills.yml @@ -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 }}"}' diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml new file mode 100644 index 0000000..2d267f5 --- /dev/null +++ b/.github/workflows/validate-pr.yml @@ -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 diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 0000000..cd2b321 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -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 diff --git a/.nvmrc b/.nvmrc index 12419e9..ffa3aec 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.18.1 \ No newline at end of file +v22.22.1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..315ff1b --- /dev/null +++ b/CHANGELOG.md @@ -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)) + + + diff --git a/package-lock.json b/package-lock.json index 45c65fa..7bb5ce5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index d0e11ad..5941e85 100644 --- a/package.json +++ b/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.1.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" } diff --git a/samples/README.md b/samples/README.md index aed2c51..9edcd00 100644 --- a/samples/README.md +++ b/samples/README.md @@ -41,3 +41,14 @@ Source is synced from the npm package [@salesforce/webapp-template-app-react-sam ### Version tracking The file `samples/webapp-template-app-react-sample-b2x-experimental/.version` stores the last-synced npm version. The Action compares it to the latest on npm and only creates a PR when they differ. + +## native-mobile-rental-tenant-app + +A sample Custom Agentic Mobile App (CAMA) for rental property tenants. This sample is maintained directly in this repository (not synced from npm). It includes: + +- **digitalExperiences** metadata: CAMA app config (`experience__camaAppMetadata`), build metadata (`experience__camaBuildMetadata`), EC definition (`experience__camaECDefinition`), and screens (`experience__camaScreen`) with tabs (Home, Tenants, Properties), theme, and toolbar settings +- **Source**: Synced from [cama-mcp-server](https://git.soma.salesforce.com/khawkins/cama-mcp-server) (branch `apply_metadata_updates`) + +### How it's used + +The sample appears on the Agentforce Vibes welcome page under the **Mobile** app type. Users can clone it via the welcome page wizard or directly from this repo. diff --git a/samples/native-mobile-rental-tenant-app/.forceignore b/samples/native-mobile-rental-tenant-app/.forceignore new file mode 100644 index 0000000..4ad8486 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/.forceignore @@ -0,0 +1,15 @@ +# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status +# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm +# + +package.xml + +# LWC configuration files +**/jsconfig.json +**/.eslintrc.json + +# LWC Jest +**/__tests__/** + +node_modules/ +.DS_Store diff --git a/samples/native-mobile-rental-tenant-app/.gitignore b/samples/native-mobile-rental-tenant-app/.gitignore new file mode 100644 index 0000000..55dce46 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +node_modules/ + +# Salesforce +.sfdx/ +.localdevserver/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* diff --git a/samples/native-mobile-rental-tenant-app/README.md b/samples/native-mobile-rental-tenant-app/README.md new file mode 100644 index 0000000..f31815b --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/README.md @@ -0,0 +1,9 @@ +# Native Mobile Rental Tenant App + +A sample Custom Agentic Mobile App (CAMA) for property managers and tenants. Use this as a starting point for building native mobile apps with Salesforce Mobile Publisher, MCF (Mobile Component Framework), and SharedUI. + +## Overview + +This sample demonstrates a rental tenant experience—browse properties, submit maintenance requests, pay rent, and communicate with property management—all from a native iOS or Android app. + + diff --git a/samples/native-mobile-rental-tenant-app/config/project-scratch-def.json b/samples/native-mobile-rental-tenant-app/config/project-scratch-def.json new file mode 100644 index 0000000..7635992 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/config/project-scratch-def.json @@ -0,0 +1,10 @@ +{ + "orgName": "Rental Tenant Dev", + "edition": "Developer", + "features": [], + "settings": { + "mobileSettings": { + "enableS1EncryptedStoragePref2": false + } + } +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaAppMetadata/appMetadata/_meta.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaAppMetadata/appMetadata/_meta.json new file mode 100644 index 0000000..89e4386 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaAppMetadata/appMetadata/_meta.json @@ -0,0 +1,5 @@ +{ + "apiName": "appMetadata", + "type": "experience__camaAppMetadata", + "path": "appMetadata" +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaAppMetadata/appMetadata/content.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaAppMetadata/appMetadata/content.json new file mode 100644 index 0000000..e60f664 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaAppMetadata/appMetadata/content.json @@ -0,0 +1,41 @@ +{ + "type": "experience__camaAppMetadata", + "title": "App Metadata", + "contentBody": { + "name": "Homestead", + "description": "Mobile application for managing property rentals, tenant information, lease agreements, and maintenance requests.", + "version": "1.0.0", + "url": "http://login.test1.pc-rnd.salesforce.com/", + "templateID": "1", + "tabs": [ + { + "id": "home", + "label": "Home", + "icon": "house.fill", + "screen": "homeScreen" + }, + { + "id": "tenants", + "label": "Tenants", + "icon": "person.2.fill", + "screen": "tenantsScreen" + }, + { + "id": "properties", + "label": "Properties", + "icon": "building.2.fill", + "screen": "propertiesScreen" + } + ], + "toolbarActions": { + "notifications": false, + "search": false, + "agentforce": false + }, + "theme": { + "accentColor": "#9400D3", + "primaryColor": "#000000", + "secondaryColor": "#B3B3B3" + } + } +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaBuildMetadata/buildMetadata/_meta.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaBuildMetadata/buildMetadata/_meta.json new file mode 100644 index 0000000..f188c58 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaBuildMetadata/buildMetadata/_meta.json @@ -0,0 +1,5 @@ +{ + "apiName": "buildMetadata", + "type": "experience__camaBuildMetadata", + "path": "buildMetadata" +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaBuildMetadata/buildMetadata/content.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaBuildMetadata/buildMetadata/content.json new file mode 100644 index 0000000..86f0450 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaBuildMetadata/buildMetadata/content.json @@ -0,0 +1,8 @@ +{ + "type": "experience__camaBuildMetadata", + "title": "Build Metadata", + "contentBody": { + "BiometricOptIn": false, + "NotificationOptIn": false + } +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaECDefinition/rentalApp/_meta.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaECDefinition/rentalApp/_meta.json new file mode 100644 index 0000000..b04fbca --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaECDefinition/rentalApp/_meta.json @@ -0,0 +1,5 @@ +{ + "apiName": "rentalApp", + "type": "experience__camaECDefinition", + "path": "rentalApp" +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaECDefinition/rentalApp/content.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaECDefinition/rentalApp/content.json new file mode 100644 index 0000000..3cb236c --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaECDefinition/rentalApp/content.json @@ -0,0 +1,5 @@ +{ + "type": "experience__camaECDefinition", + "title": "CAMA EC Definition", + "contentBody": {} +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/homeScreen/_meta.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/homeScreen/_meta.json new file mode 100644 index 0000000..c2d0d5d --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/homeScreen/_meta.json @@ -0,0 +1,5 @@ +{ + "apiName": "homeScreen", + "type": "experience__camaScreen", + "path": "homeScreen" +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/homeScreen/content.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/homeScreen/content.json new file mode 100644 index 0000000..2f89f6c --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/homeScreen/content.json @@ -0,0 +1,489 @@ +{ + "type": "experience__camaScreen", + "title": "Home Screen", + "contentBody": { + "view": { + "definition": "homestead/HomeScreen", + "properties": { + "pullToRefresh": true + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/container", + "properties": { + "padding": "$spacing.spacing4", + "backgroundColor": "$colors.surface2" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/card", + "properties": {}, + "regions": { + "components": { + "components": [ + { + "definition": "ui/column", + "properties": { + "width": "fill", + "height": "fill", + "alignment": "start", + "gap": "$spacing.spacing4", + "padding": "$spacing.spacing4" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/text", + "properties": { + "text": "Today", + "style": "titlesFontScale4Semibold", + "color": "$colors.onSurface1" + }, + "regions": {} + }, + { + "definition": "ui/text", + "properties": { + "text": "You have no event.", + "color": "$colors.onSuccess1", + "style": "bodyFontScale2Regular" + }, + "regions": {} + } + ] + } + } + } + ] + } + } + }, + { + "definition": "ui/card", + "properties": {}, + "regions": { + "components": { + "components": [ + { + "definition": "ui/column", + "properties": { + "width": "fill", + "height": "fill", + "alignment": "start", + "gap": "$spacing.spacing4", + "padding": "$spacing.spacing4" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/text", + "properties": { + "text": "Upcoming", + "style": "titlesFontScale4Semibold", + "color": "$colors.onSurface1" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "123 Oak Street", + "subtitle": "Inspection on 01/08/2026" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/nothing" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Scheduled", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "456 Maple Avenue", + "subtitle": "Lease Signing on 01/10/2026" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/nothing" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Scheduled", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "789 Pine Road", + "subtitle": "Maintenance on 01/15/2026" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/nothing" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Requested", + "variant": "error" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "321 Elm Street", + "subtitle": "Rent Collection on 01/20/2026" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/nothing" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Scheduled", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Community Center", + "subtitle": "HOA Meeting on 01/22/2026" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/nothing" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Proposed", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "555 Cedar Lane", + "subtitle": "Background Check" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/nothing" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "In Progress", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + } + ] + } + } + }, + { + "definition": "ui/card", + "properties": {}, + "regions": { + "components": { + "components": [ + { + "definition": "ui/column", + "properties": { + "width": "fill", + "height": "fill", + "alignment": "start", + "gap": "$spacing.spacing4", + "padding": "$spacing.spacing4" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/text", + "properties": { + "text": "Tenants", + "style": "titlesFontScale4Semibold", + "color": "$colors.onSurface1" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Sarah Johnson", + "subtitle": "456 Maple Avenue, Apt 5" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Michael Chen", + "subtitle": "789 Pine Road" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Overdue", + "variant": "error" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Emily Davis", + "subtitle": "321 Elm Street, Unit 5B" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Robert Williams", + "subtitle": "555 Cedar Lane" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Jessica Martinez", + "subtitle": "123 Oak Street, Unit 3C" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Moved Out", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "David Lee", + "subtitle": "456 Maple Avenue, Apt 2" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Signed", + "variant": "lightest" + }, + "regions": {} + } + ] + } + } + } + ] + } + } + } + ] + } + } + } + ] + } + } + }, + "target": "native__homesteadHome", + "apiName": "uem_homesteadHome" + } +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/propertiesScreen/_meta.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/propertiesScreen/_meta.json new file mode 100644 index 0000000..2446d28 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/propertiesScreen/_meta.json @@ -0,0 +1,5 @@ +{ + "apiName": "propertiesScreen", + "type": "experience__camaScreen", + "path": "propertiesScreen" +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/propertiesScreen/content.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/propertiesScreen/content.json new file mode 100644 index 0000000..4412998 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/propertiesScreen/content.json @@ -0,0 +1,565 @@ +{ + "type": "experience__camaScreen", + "title": "Properties Screen", + "contentBody": { + "view": { + "definition": "homestead/PropertiesScreen", + "properties": { + "pullToRefresh": true + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/container", + "properties": { + "padding": "$spacing.spacing4", + "backgroundColor": "$colors.surface2" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/card", + "properties": {}, + "regions": { + "components": { + "components": [ + { + "definition": "ui/baseRow", + "properties": { + "title": "Oak Street Apartments", + "subtitle": "123 Oak Street" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Maple Avenue Complex", + "subtitle": "456 Maple Avenue" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Partial", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Pine Road House", + "subtitle": "789 Pine Road" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Vacant", + "variant": "lightest" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Cedar Lane Condos", + "subtitle": "555 Cedar Lane" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Elm Street Tower", + "subtitle": "321 Elm Street" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Partial", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Birch Boulevard Villa", + "subtitle": "234 Birch Boulevard" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Willow Way Estates", + "subtitle": "890 Willow Way" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Vacant", + "variant": "lightest" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Sunset Plaza", + "subtitle": "567 Sunset Drive" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Riverside Apartments", + "subtitle": "678 River Road" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Partial", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Lakeside Manor", + "subtitle": "901 Lake Avenue" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Mountain View Heights", + "subtitle": "112 Mountain Road" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Maintenance", + "variant": "error" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Harbor Point Lofts", + "subtitle": "445 Harbor Street" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Partial", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Garden Terrace", + "subtitle": "223 Garden Lane" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Park Place Residences", + "subtitle": "334 Park Avenue" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Vacant", + "variant": "lightest" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Hillside Court", + "subtitle": "778 Hill Street" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Downtown Tower", + "subtitle": "990 Main Street" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Partial", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Bayside Commons", + "subtitle": "101 Bay Road" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Full", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Meadow Brook Village", + "subtitle": "212 Meadow Lane" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/home", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Maintenance", + "variant": "error" + }, + "regions": {} + } + ] + } + } + } + ] + } + } + } + ] + } + } + } + ] + } + } + }, + "target": "native__homesteadProperties", + "apiName": "uem_homesteadProperties" + } +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/tenantsScreen/_meta.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/tenantsScreen/_meta.json new file mode 100644 index 0000000..fc4592a --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/tenantsScreen/_meta.json @@ -0,0 +1,5 @@ +{ + "apiName": "tenantsScreen", + "type": "experience__camaScreen", + "path": "tenantsScreen" +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/tenantsScreen/content.json b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/tenantsScreen/content.json new file mode 100644 index 0000000..9100149 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/experience__camaScreen/tenantsScreen/content.json @@ -0,0 +1,478 @@ +{ + "type": "experience__camaScreen", + "title": "Tenants Screen", + "contentBody": { + "view": { + "definition": "homestead/TenantsScreen", + "properties": { + "pullToRefresh": true + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/container", + "properties": { + "padding": "$spacing.spacing4", + "backgroundColor": "$colors.surface2" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/card", + "properties": {}, + "regions": { + "components": { + "components": [ + { + "definition": "ui/baseRow", + "properties": { + "title": "Sarah Johnson", + "subtitle": "456 Maple Avenue, Apt 5" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Michael Chen", + "subtitle": "789 Pine Road" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Overdue", + "variant": "error" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Emily Davis", + "subtitle": "321 Elm Street, Unit 5B" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Robert Williams", + "subtitle": "555 Cedar Lane" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Jessica Martinez", + "subtitle": "123 Oak Street, Unit 3C" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Moved Out", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "David Lee", + "subtitle": "456 Maple Avenue, Apt 2" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Signed", + "variant": "lightest" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Amanda Thompson", + "subtitle": "789 Pine Road, Unit 7A" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Christopher Garcia", + "subtitle": "321 Elm Street, Apt 8" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Overdue", + "variant": "error" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Nicole Anderson", + "subtitle": "555 Cedar Lane, Unit 12B" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "James Wilson", + "subtitle": "123 Oak Street, Apt 1A" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Pending", + "variant": "warning" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Rachel Brown", + "subtitle": "456 Maple Avenue, Unit 9C" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Kevin Taylor", + "subtitle": "789 Pine Road, Apt 4B" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Signed", + "variant": "lightest" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Stephanie Miller", + "subtitle": "321 Elm Street, Unit 6D" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Brian Martinez", + "subtitle": "555 Cedar Lane, Apt 10A" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Overdue", + "variant": "error" + }, + "regions": {} + } + ] + } + } + }, + { + "definition": "ui/baseRow", + "properties": { + "title": "Laura Rodriguez", + "subtitle": "123 Oak Street, Unit 11B" + }, + "regions": { + "components": { + "components": [ + { + "definition": "ui/utilityIcon", + "properties": { + "iconName": "utility/user", + "size": "small" + }, + "regions": {} + }, + { + "definition": "ui/badge", + "properties": { + "label": "Paid", + "variant": "success" + }, + "regions": {} + } + ] + } + } + } + ] + } + } + } + ] + } + } + } + ] + } + } + }, + "target": "native__homesteadTenants", + "apiName": "uem_homesteadTenants" + } +} diff --git a/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/rentalApp.digitalExperience-meta.xml b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/rentalApp.digitalExperience-meta.xml new file mode 100644 index 0000000..b93075a --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/force-app/main/default/digitalExperiences/experiencecontainer/rentalApp/rentalApp.digitalExperience-meta.xml @@ -0,0 +1,4 @@ + + + + diff --git a/samples/native-mobile-rental-tenant-app/package.json b/samples/native-mobile-rental-tenant-app/package.json new file mode 100644 index 0000000..c384ac3 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "native-mobile-rental-tenant-app", + "version": "1.0.0", + "description": "Sample CAMA app for rental property tenants", + "private": true, + "scripts": { + "build": "echo 'No build required for base project'", + "clean": "echo 'No clean required'", + "lint": "eslint **/{aura,lwc}/**/*.js 2>/dev/null || true", + "test": "echo 'Add tests when ready'" + }, + "devDependencies": {} +} diff --git a/samples/native-mobile-rental-tenant-app/sfdx-project.json b/samples/native-mobile-rental-tenant-app/sfdx-project.json new file mode 100644 index 0000000..cb22612 --- /dev/null +++ b/samples/native-mobile-rental-tenant-app/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "RentalTenantApp", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "66.0" +} diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts new file mode 100644 index 0000000..9087b52 --- /dev/null +++ b/scripts/validate-skills.ts @@ -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 | 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 | 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 = {} + 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() diff --git a/skills/apex-development/apex-class/SKILL.md b/skills/apex-class/SKILL.md similarity index 100% rename from skills/apex-development/apex-class/SKILL.md rename to skills/apex-class/SKILL.md diff --git a/skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls b/skills/apex-class/examples/AccountDeduplicationBatch.cls similarity index 100% rename from skills/apex-development/apex-class/examples/AccountDeduplicationBatch.cls rename to skills/apex-class/examples/AccountDeduplicationBatch.cls diff --git a/skills/apex-development/apex-class/examples/AccountSelector.cls b/skills/apex-class/examples/AccountSelector.cls similarity index 100% rename from skills/apex-development/apex-class/examples/AccountSelector.cls rename to skills/apex-class/examples/AccountSelector.cls diff --git a/skills/apex-development/apex-class/examples/AccountService.cls b/skills/apex-class/examples/AccountService.cls similarity index 100% rename from skills/apex-development/apex-class/examples/AccountService.cls rename to skills/apex-class/examples/AccountService.cls diff --git a/skills/apex-development/apex-class/templates/abstract.cls b/skills/apex-class/templates/abstract.cls similarity index 100% rename from skills/apex-development/apex-class/templates/abstract.cls rename to skills/apex-class/templates/abstract.cls diff --git a/skills/apex-development/apex-class/templates/batch.cls b/skills/apex-class/templates/batch.cls similarity index 100% rename from skills/apex-development/apex-class/templates/batch.cls rename to skills/apex-class/templates/batch.cls diff --git a/skills/apex-development/apex-class/templates/domain.cls b/skills/apex-class/templates/domain.cls similarity index 100% rename from skills/apex-development/apex-class/templates/domain.cls rename to skills/apex-class/templates/domain.cls diff --git a/skills/apex-development/apex-class/templates/dto.cls b/skills/apex-class/templates/dto.cls similarity index 100% rename from skills/apex-development/apex-class/templates/dto.cls rename to skills/apex-class/templates/dto.cls diff --git a/skills/apex-development/apex-class/templates/exception.cls b/skills/apex-class/templates/exception.cls similarity index 100% rename from skills/apex-development/apex-class/templates/exception.cls rename to skills/apex-class/templates/exception.cls diff --git a/skills/apex-development/apex-class/templates/interface.cls b/skills/apex-class/templates/interface.cls similarity index 100% rename from skills/apex-development/apex-class/templates/interface.cls rename to skills/apex-class/templates/interface.cls diff --git a/skills/apex-development/apex-class/templates/queueable.cls b/skills/apex-class/templates/queueable.cls similarity index 100% rename from skills/apex-development/apex-class/templates/queueable.cls rename to skills/apex-class/templates/queueable.cls diff --git a/skills/apex-development/apex-class/templates/schedulable.cls b/skills/apex-class/templates/schedulable.cls similarity index 100% rename from skills/apex-development/apex-class/templates/schedulable.cls rename to skills/apex-class/templates/schedulable.cls diff --git a/skills/apex-development/apex-class/templates/selector.cls b/skills/apex-class/templates/selector.cls similarity index 100% rename from skills/apex-development/apex-class/templates/selector.cls rename to skills/apex-class/templates/selector.cls diff --git a/skills/apex-development/apex-class/templates/service.cls b/skills/apex-class/templates/service.cls similarity index 100% rename from skills/apex-development/apex-class/templates/service.cls rename to skills/apex-class/templates/service.cls diff --git a/skills/apex-development/apex-class/templates/utility.cls b/skills/apex-class/templates/utility.cls similarity index 100% rename from skills/apex-development/apex-class/templates/utility.cls rename to skills/apex-class/templates/utility.cls diff --git a/skills/testing-automation/apex-test-class/SKILL.md b/skills/apex-test-class/SKILL.md similarity index 100% rename from skills/testing-automation/apex-test-class/SKILL.md rename to skills/apex-test-class/SKILL.md diff --git a/skills/testing-automation/apex-test-class/references/assertion-patterns.md b/skills/apex-test-class/references/assertion-patterns.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/assertion-patterns.md rename to skills/apex-test-class/references/assertion-patterns.md diff --git a/skills/testing-automation/apex-test-class/references/async-testing.md b/skills/apex-test-class/references/async-testing.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/async-testing.md rename to skills/apex-test-class/references/async-testing.md diff --git a/skills/testing-automation/apex-test-class/references/mocking-patterns.md b/skills/apex-test-class/references/mocking-patterns.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/mocking-patterns.md rename to skills/apex-test-class/references/mocking-patterns.md diff --git a/skills/testing-automation/apex-test-class/references/test-data-factory.md b/skills/apex-test-class/references/test-data-factory.md similarity index 100% rename from skills/testing-automation/apex-test-class/references/test-data-factory.md rename to skills/apex-test-class/references/test-data-factory.md diff --git a/skills/metadata-deployments/deployment-readiness-check/SKILL.md b/skills/deployment-readiness-check/SKILL.md similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/SKILL.md rename to skills/deployment-readiness-check/SKILL.md diff --git a/skills/metadata-deployments/deployment-readiness-check/assets/deployment_checklist.md b/skills/deployment-readiness-check/assets/deployment_checklist.md similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/assets/deployment_checklist.md rename to skills/deployment-readiness-check/assets/deployment_checklist.md diff --git a/skills/metadata-deployments/deployment-readiness-check/references/rollback_procedures.md b/skills/deployment-readiness-check/references/rollback_procedures.md similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/references/rollback_procedures.md rename to skills/deployment-readiness-check/references/rollback_procedures.md diff --git a/skills/metadata-deployments/deployment-readiness-check/scripts/check_metadata.sh b/skills/deployment-readiness-check/scripts/check_metadata.sh similarity index 100% rename from skills/metadata-deployments/deployment-readiness-check/scripts/check_metadata.sh rename to skills/deployment-readiness-check/scripts/check_metadata.sh diff --git a/skills/generate-permission-set/SKILL.md b/skills/generate-permission-set/SKILL.md new file mode 100644 index 0000000..791e5e1 --- /dev/null +++ b/skills/generate-permission-set/SKILL.md @@ -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 + + YourPermissionSetName + + Clear description of purpose and intended audience + +``` + +**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 + + true + true + true + false + false + false + false + Account + +``` + +## Step 3: Set Field-Level Security + +Define field permissions for sensitive or custom fields: + +```xml + + true + true + Account.SSN__c + +``` + +**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 `true`: +```xml + + FieldName__c + true + +``` +- 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 + + true + ApiEnabled + + + true + RunReports + +``` + +**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 + + Sales_Console + true + + + CustomTab__c + Visible + +``` + +**Application visibility options:** +- 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 + + CustomController + true + + + CustomPage + true + +``` + +## Step 7: Set License and Record Type Settings (Optional) + +Specify license requirements and record type visibility: + +```xml +Salesforce +false + + Account.Business + true + true + +``` + +## Validation Checklist + +Before deploying, verify: +- [ ] fullName, label, description set +- [ ] Permissions follow least privilege +- [ ] No required fields in `` +- [ ] No duplicate permissions +- [ ] no lengthy comments + +## What Causes Deployment Failure + +- **Field permissions on required fields:** Any required field in `` 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 \ No newline at end of file diff --git a/skills/salesforce-flow/SKILL.md b/skills/salesforce-flow/SKILL.md index e3a55d5..4513457 100644 --- a/skills/salesforce-flow/SKILL.md +++ b/skills/salesforce-flow/SKILL.md @@ -1,6 +1,6 @@ --- 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 @@ -21,7 +21,7 @@ Use this skill when you need to: # Flow Metadata Specification ## 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 - Automate complex business processes with declarative logic and branching diff --git a/skills/apex-development/trigger-refactor-pipeline/SKILL.md b/skills/trigger-refactor-pipeline/SKILL.md similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/SKILL.md rename to skills/trigger-refactor-pipeline/SKILL.md diff --git a/skills/apex-development/trigger-refactor-pipeline/assets/test_template.apex b/skills/trigger-refactor-pipeline/assets/test_template.apex similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/assets/test_template.apex rename to skills/trigger-refactor-pipeline/assets/test_template.apex diff --git a/skills/apex-development/trigger-refactor-pipeline/references/handler_patterns.md b/skills/trigger-refactor-pipeline/references/handler_patterns.md similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/references/handler_patterns.md rename to skills/trigger-refactor-pipeline/references/handler_patterns.md diff --git a/skills/apex-development/trigger-refactor-pipeline/scripts/analyze_trigger.py b/skills/trigger-refactor-pipeline/scripts/analyze_trigger.py similarity index 100% rename from skills/apex-development/trigger-refactor-pipeline/scripts/analyze_trigger.py rename to skills/trigger-refactor-pipeline/scripts/analyze_trigger.py