|
23 | 23 | SKILL_METADATA = { |
24 | 24 | "databricks-core": { |
25 | 25 | "description": "Core Databricks skill for CLI, auth, and data exploration", |
| 26 | + "plugin_keyword": "cli", |
26 | 27 | }, |
27 | 28 | "databricks-apps": { |
28 | 29 | "description": "Databricks Apps development and deployment (evaluates analytics vs synced tables data access)", |
| 30 | + "plugin_keyword": "apps", |
29 | 31 | }, |
30 | 32 | "databricks-jobs": { |
31 | 33 | "description": "Develop and deploy Lakeflow Jobs on Databricks via DABs, Python SDK, or the CLI — covers all task types, triggers, notifications, and worked examples", |
| 34 | + "plugin_keyword": "jobs", |
32 | 35 | }, |
33 | 36 | "databricks-lakebase": { |
34 | 37 | "description": "Databricks Lakebase Postgres: projects, scaling, connectivity, synced tables, and Data API", |
| 38 | + "plugin_keyword": "lakebase", |
35 | 39 | }, |
36 | 40 | "databricks-dabs": { |
37 | 41 | "description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources", |
| 42 | + "plugin_keyword": "dabs", |
38 | 43 | }, |
39 | 44 | "databricks-model-serving": { |
40 | 45 | "description": "Databricks Model Serving endpoint management", |
| 46 | + "plugin_keyword": "model-serving", |
41 | 47 | }, |
42 | 48 | "databricks-pipelines": { |
43 | 49 | "description": "Databricks Spark Declarative Pipelines (SDP) for ETL and streaming", |
| 50 | + "plugin_keyword": "pipelines", |
44 | 51 | }, |
45 | 52 | "databricks-serverless-migration": { |
46 | 53 | "description": "Migrate Databricks workloads from classic compute to serverless compute, including compatibility checks and concrete fixes", |
| 54 | + "plugin_keyword": "serverless", |
47 | 55 | }, |
48 | 56 | } |
49 | 57 |
|
@@ -422,6 +430,69 @@ def validate_manifest(repo_root: Path) -> bool: |
422 | 430 | return True |
423 | 431 |
|
424 | 432 |
|
| 433 | +def validate_plugin_manifests(repo_root: Path) -> list[str]: |
| 434 | + """Validate .claude-plugin/plugin.json and .claude-plugin/marketplace.json |
| 435 | + stay in sync with the set of skills shipped in this repo. |
| 436 | +
|
| 437 | + The two manifests power Claude Code marketplace discovery; if a new skill |
| 438 | + lands without a corresponding plugin keyword bump, the marketplace listing |
| 439 | + silently goes stale. This check forces SKILL_METADATA, plugin.json, and |
| 440 | + marketplace.json to stay aligned. |
| 441 | +
|
| 442 | + Returns a list of error strings (empty means all good). |
| 443 | + """ |
| 444 | + errors: list[str] = [] |
| 445 | + |
| 446 | + plugin_path = repo_root / ".claude-plugin" / "plugin.json" |
| 447 | + marketplace_path = repo_root / ".claude-plugin" / "marketplace.json" |
| 448 | + |
| 449 | + if not plugin_path.exists(): |
| 450 | + errors.append(f"Missing {plugin_path.relative_to(repo_root)}") |
| 451 | + if not marketplace_path.exists(): |
| 452 | + errors.append(f"Missing {marketplace_path.relative_to(repo_root)}") |
| 453 | + if errors: |
| 454 | + return errors |
| 455 | + |
| 456 | + plugin = json.loads(plugin_path.read_text()) |
| 457 | + marketplace = json.loads(marketplace_path.read_text()) |
| 458 | + |
| 459 | + keywords = {k.lower() for k in plugin.get("keywords", [])} |
| 460 | + |
| 461 | + for skill_dir in iter_skill_dirs(repo_root): |
| 462 | + meta = SKILL_METADATA.get(skill_dir.name) |
| 463 | + if meta is None: |
| 464 | + continue # generate_manifest will flag the missing metadata |
| 465 | + expected_keyword = meta.get("plugin_keyword") |
| 466 | + if not expected_keyword: |
| 467 | + errors.append( |
| 468 | + f"SKILL_METADATA['{skill_dir.name}'] is missing 'plugin_keyword'. " |
| 469 | + "Add one so .claude-plugin/plugin.json keywords coverage can be validated." |
| 470 | + ) |
| 471 | + continue |
| 472 | + if expected_keyword.lower() not in keywords: |
| 473 | + errors.append( |
| 474 | + f"Skill '{skill_dir.name}' has no corresponding entry in " |
| 475 | + f".claude-plugin/plugin.json 'keywords' (looking for '{expected_keyword}'). " |
| 476 | + "Add it so the Claude marketplace listing stays in sync." |
| 477 | + ) |
| 478 | + |
| 479 | + plugin_name = plugin.get("name") |
| 480 | + market_plugins = marketplace.get("plugins", []) |
| 481 | + market_entry = next((p for p in market_plugins if p.get("name") == plugin_name), None) |
| 482 | + if market_entry is None: |
| 483 | + errors.append( |
| 484 | + f".claude-plugin/marketplace.json has no entry for plugin '{plugin_name}'." |
| 485 | + ) |
| 486 | + else: |
| 487 | + if market_entry.get("description") != plugin.get("description"): |
| 488 | + errors.append( |
| 489 | + ".claude-plugin/marketplace.json plugin description must match " |
| 490 | + ".claude-plugin/plugin.json description (they drift independently otherwise)." |
| 491 | + ) |
| 492 | + |
| 493 | + return errors |
| 494 | + |
| 495 | + |
425 | 496 | # --------------------------------------------------------------------------- |
426 | 497 | # CLI |
427 | 498 | # --------------------------------------------------------------------------- |
@@ -479,9 +550,22 @@ def main() -> None: |
479 | 550 | if not validate_manifest(repo_root): |
480 | 551 | ok = False |
481 | 552 |
|
| 553 | + plugin_errors = validate_plugin_manifests(repo_root) |
| 554 | + if plugin_errors: |
| 555 | + print( |
| 556 | + "ERROR: .claude-plugin manifests are out of sync with skills:", |
| 557 | + file=sys.stderr, |
| 558 | + ) |
| 559 | + for err in plugin_errors: |
| 560 | + print(f" - {err}", file=sys.stderr) |
| 561 | + ok = False |
| 562 | + |
482 | 563 | if not ok: |
483 | 564 | print( |
484 | | - "\nRun `python3 scripts/skills.py generate` to fix.", |
| 565 | + "\nRun `python3 scripts/skills.py generate` to fix the " |
| 566 | + "manifest, and update .claude-plugin/plugin.json + " |
| 567 | + "marketplace.json by hand for any plugin-keyword/description " |
| 568 | + "mismatches.", |
485 | 569 | file=sys.stderr, |
486 | 570 | ) |
487 | 571 | sys.exit(1) |
|
0 commit comments