diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index f7fc092..4ed5eaa 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "blender-developer-tools", "displayName": "Blender Developer Tools", "description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development", - "version": "0.2.3", + "version": "0.5.0", "author": { "name": "TMHSDigital", "email": "contact@users.noreply.github.com" @@ -34,5 +34,34 @@ "rules/type-annotate-props-and-defend-context.mdc", "rules/prefer-temp-override-over-context-copy.mdc", "rules/use-foreach-set-for-bulk-data.mdc" + ], + "snippets": [ + "snippets/action-ensure-channelbag-for-slot.py", + "snippets/app-handler-registration.py", + "snippets/bmesh-load-edit-free.py", + "snippets/canonical-object-creation.py", + "snippets/canonical-object-deletion.py", + "snippets/cross-version-property-delete.py", + "snippets/depsgraph-evaluated-mesh.py", + "snippets/driver-with-custom-function.py", + "snippets/foreach-get-vertices.py", + "snippets/foreach-set-vertices.py", + "snippets/pointerproperty-binding.py", + "snippets/principled-bsdf-material.py", + "snippets/register-classes-factory.py", + "snippets/shader-node-group.py", + "snippets/temp-override-context.py", + "snippets/usd-export-evaluation-mode.py", + "snippets/version-branch-skeleton.py" + ], + "templates": [ + "templates/extension-addon-template", + "templates/headless-batch-script-template" + ], + "examples": [ + "examples/depsgraph-export", + "examples/gn-sdf-remesh", + "examples/swatch-grid", + "examples/turntable" ] } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f45b307..3369cc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -122,6 +122,27 @@ jobs: previous-version: ${{ steps.current.outputs.version }} meta-repo-ref: v1.15.1 + - name: Sync plugin manifest version + if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' + env: + NEW_VERSION: ${{ steps.new.outputs.version }} + run: | + python3 - << 'PYEOF' + import os + import re + + path = '.cursor-plugin/plugin.json' + text = open(path).read() + text = re.sub( + r'^( "version": ")[^"]+(",)$', + rf'\g<1>{os.environ["NEW_VERSION"]}\g<2>', + text, + count=1, + flags=re.M, + ) + open(path, 'w').write(text) + PYEOF + - name: Commit version bump if: steps.check.outputs.skip == 'false' && steps.bump.outputs.release == 'true' run: | diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d8e45cd..60b60be 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -157,6 +157,62 @@ jobs: echo "Templates: $template_count" echo "Snippets: $snippet_count" + validate-manifest: + name: Validate plugin manifest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Check plugin.json matches filesystem and VERSION + run: | + python3 << 'PYEOF' + import glob + import json + import os + import sys + + errors = [] + manifest = json.load(open('.cursor-plugin/plugin.json')) + + version = open('VERSION').read().strip() + if manifest.get('version') != version: + errors.append( + f"plugin.json version '{manifest.get('version')}' != VERSION '{version}'" + ) + + # Every manifest path must exist on disk. + for key in ('skills', 'rules', 'snippets', 'templates', 'examples'): + for path in manifest.get(key, []): + if not os.path.exists(path): + errors.append(f'{key}: manifest lists missing path {path}') + + # Every content file/dir on disk must be listed in the manifest. + expected = { + 'skills': sorted(glob.glob('skills/*/SKILL.md')), + 'rules': sorted(glob.glob('rules/*.mdc')), + 'snippets': sorted(glob.glob('snippets/*.py')), + 'templates': sorted( + d for d in glob.glob('templates/*') if os.path.isdir(d) + ), + 'examples': sorted( + d for d in glob.glob('examples/*') if os.path.isdir(d) + ), + } + for key, paths in expected.items(): + listed = {p.replace('\\', '/') for p in manifest.get(key, [])} + for path in paths: + if path.replace('\\', '/') not in listed: + errors.append(f'{key}: {path} on disk but not in manifest') + + if errors: + for e in errors: + print(f'::error::{e}', file=sys.stderr) + sys.exit(1) + + counts = {k: len(manifest.get(k, [])) for k in expected} + print(f'Manifest verified at v{version}: {counts}') + PYEOF + validate-counts: name: Validate content counts runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index b620074..ec5e953 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,12 +111,18 @@ way, and a one-paragraph rationale. 30 to 80 lines is the right size. asserts the README aggregate counts (12 skills, 6 rules, 2 templates, 17 snippets) match filesystem reality. The counts language in `README.md` is load-bearing: the job greps for it. +- `validate.yml` also runs a `validate-manifest` job that checks + `.cursor-plugin/plugin.json` against reality: every listed path must exist, + every skill, rule, snippet, template, and example on disk must be listed, + and the manifest `version` must equal `VERSION`. The release pipeline owns + the manifest `version` line (see `release.yml` below) — never hand-edit it. - `drift-check.yml` consumes `Developer-Tools-Directory/.github/actions/ drift-check@v1.15` to enforce ecosystem standards-version markers. - `release.yml` auto-bumps the version, tags, force-updates floating tags `v0` and `v0.1`, and runs `release-doc-sync@v1` to rewrite CHANGELOG.md, - CLAUDE.md `**Version:**`, and ROADMAP.md `**Current:**`. Triggered on - push to `main` for content-changing paths only. + CLAUDE.md `**Version:**`, and ROADMAP.md `**Current:**`. It also rewrites + the `"version"` line in `.cursor-plugin/plugin.json` so the manifest tracks + each release. Triggered on push to `main` for content-changing paths only. - `label-sync.yml` self-heals labels via `gh label create --force` per label, then applies them to the PR.