diff --git a/.github/workflows/validate-manifest.yml b/.github/workflows/validate-manifest.yml index a155482..0b7fe42 100644 --- a/.github/workflows/validate-manifest.yml +++ b/.github/workflows/validate-manifest.yml @@ -4,7 +4,8 @@ on: pull_request: paths: - 'skills/**' - - 'scripts/generate_manifest.py' + - 'assets/**' + - 'scripts/skills.py' - 'manifest.json' push: branches: @@ -24,4 +25,4 @@ jobs: python-version: '3.11' - name: Validate manifest is up to date - run: python3 scripts/generate_manifest.py validate + run: python3 scripts/skills.py validate diff --git a/CLAUDE.md b/CLAUDE.md index 9a84e00..19236df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,11 +37,12 @@ Then apply these patterns: - Pattern 2 ``` -### Manifest +### Skills management ```bash -python3 scripts/generate_manifest.py # generate -python3 scripts/generate_manifest.py validate # check in CI +python3 scripts/skills.py # sync assets + generate manifest (default) +python3 scripts/skills.py sync # copy shared assets into each skill dir +python3 scripts/skills.py validate # check assets + manifest are up to date (CI) ``` ## Security diff --git a/README.md b/README.md index 4a62124..d49a6ae 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,16 @@ This approach: ### Manifest Management -Generate manifest after adding/updating skills: +Sync assets and generate manifest after adding/updating skills: ```bash -python3 scripts/generate_manifest.py +python3 scripts/skills.py ``` -Validate that manifest is up to date (for CI): +Validate that assets and manifest are up to date (for CI): ```bash -python3 scripts/generate_manifest.py validate +python3 scripts/skills.py validate ``` The manifest is used by the CLI to discover available skills. diff --git a/assets/databricks.png b/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/assets/databricks.png differ diff --git a/assets/databricks.svg b/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json index b28e975..ffd9ca8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,14 +1,17 @@ { "version": "2", - "updated_at": "2026-04-02T10:44:39Z", + "updated_at": "2026-04-07T12:45:02Z", "skills": { "databricks-apps": { "version": "0.1.1", "description": "Databricks Apps development and deployment", "experimental": false, - "updated_at": "2026-04-02T10:44:20Z", + "updated_at": "2026-04-07T12:41:53Z", "files": [ "SKILL.md", + "agents/openai.yaml", + "assets/databricks.png", + "assets/databricks.svg", "references/appkit/appkit-sdk.md", "references/appkit/frontend.md", "references/appkit/lakebase.md", @@ -26,9 +29,12 @@ "version": "0.1.0", "description": "Core Databricks skill for CLI, auth, and data exploration", "experimental": false, - "updated_at": "2026-04-02T10:44:20Z", + "updated_at": "2026-04-07T12:41:53Z", "files": [ "SKILL.md", + "agents/openai.yaml", + "assets/databricks.png", + "assets/databricks.svg", "data-exploration.md", "databricks-cli-auth.md", "databricks-cli-install.md" @@ -38,9 +44,12 @@ "version": "0.0.0", "description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources", "experimental": false, - "updated_at": "2026-04-02T10:44:20Z", + "updated_at": "2026-04-07T12:44:49Z", "files": [ "SKILL.md", + "agents/openai.yaml", + "assets/databricks.png", + "assets/databricks.svg", "references/alerts.md", "references/bundle-structure.md", "references/deploy-and-run.md", @@ -53,27 +62,36 @@ "version": "0.1.0", "description": "Databricks Jobs orchestration and scheduling", "experimental": false, - "updated_at": "2026-04-02T10:44:20Z", + "updated_at": "2026-04-07T12:41:53Z", "files": [ - "SKILL.md" + "SKILL.md", + "agents/openai.yaml", + "assets/databricks.png", + "assets/databricks.svg" ] }, "databricks-lakebase": { "version": "0.1.0", "description": "Databricks Lakebase database development", "experimental": false, - "updated_at": "2026-04-02T10:44:20Z", + "updated_at": "2026-04-07T12:41:53Z", "files": [ - "SKILL.md" + "SKILL.md", + "agents/openai.yaml", + "assets/databricks.png", + "assets/databricks.svg" ] }, "databricks-pipelines": { "version": "0.1.0", "description": "Databricks Pipelines (DLT) for ETL and streaming", "experimental": false, - "updated_at": "2026-04-02T10:44:20Z", + "updated_at": "2026-04-07T12:41:53Z", "files": [ "SKILL.md", + "agents/openai.yaml", + "assets/databricks.png", + "assets/databricks.svg", "references/auto-cdc-python.md", "references/auto-cdc-sql.md", "references/auto-cdc.md", diff --git a/scripts/generate_manifest.py b/scripts/skills.py similarity index 52% rename from scripts/generate_manifest.py rename to scripts/skills.py index 13c8301..b319438 100644 --- a/scripts/generate_manifest.py +++ b/scripts/skills.py @@ -1,14 +1,65 @@ #!/usr/bin/env python3 -"""Generate or validate manifest.json from skill directories.""" +"""Manage skills: sync shared assets, generate manifest, validate.""" import argparse import json import re +import shutil import sys from datetime import datetime, timezone from pathlib import Path +SHARED_ASSETS = [ + "assets/databricks.svg", + "assets/databricks.png", +] + +SKILL_METADATA = { + "databricks-core": { + "description": "Core Databricks skill for CLI, auth, and data exploration", + "experimental": False, + }, + "databricks-apps": { + "description": "Databricks Apps development and deployment", + "experimental": False, + }, + "databricks-jobs": { + "description": "Databricks Jobs orchestration and scheduling", + "experimental": False, + }, + "databricks-lakebase": { + "description": "Databricks Lakebase database development", + "experimental": False, + }, + "databricks-dabs": { + "description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources", + "experimental": False, + }, + "databricks-pipelines": { + "description": "Databricks Pipelines (DLT) for ETL and streaming", + "experimental": False, + }, + "databricks-proto-first": { + "description": "Proto-first schema design for Databricks apps", + "experimental": False, + }, +} + + +def iter_skill_dirs(repo_root: Path): + """Yield skill directories that contain SKILL.md.""" + skills_dir = repo_root / "skills" + for item in sorted(skills_dir.iterdir()): + if not item.is_dir(): + continue + if item.name.startswith(".") or item.name == "scripts": + continue + if not (item / "SKILL.md").exists(): + continue + yield item + + def extract_version_from_skill(skill_path: Path) -> str: """Extract version from SKILL.md frontmatter metadata.""" skill_md = skill_path / "SKILL.md" @@ -17,7 +68,6 @@ def extract_version_from_skill(skill_path: Path) -> str: content = skill_md.read_text() - # parse YAML frontmatter if not content.startswith("---"): raise ValueError(f"SKILL.md in {skill_path} missing frontmatter") @@ -27,7 +77,6 @@ def extract_version_from_skill(skill_path: Path) -> str: frontmatter = content[3:end_idx] - # extract version from metadata block version_match = re.search(r'version:\s*["\']?([^"\'\n]+)["\']?', frontmatter) if version_match: return version_match.group(1).strip() @@ -52,86 +101,111 @@ def get_skill_updated_at(skill_path: Path) -> str: ) -SKILL_METADATA = { - "databricks-core": { - "description": "Core Databricks skill for CLI, auth, and data exploration", - "experimental": False, - }, - "databricks-apps": { - "description": "Databricks Apps development and deployment", - "experimental": False, - }, - "databricks-jobs": { - "description": "Databricks Jobs orchestration and scheduling", - "experimental": False, - }, - "databricks-lakebase": { - "description": "Databricks Lakebase database development", - "experimental": False, - }, - "databricks-dabs": { - "description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources", - "experimental": False, - }, - "databricks-pipelines": { - "description": "Databricks Pipelines (DLT) for ETL and streaming", - "experimental": False, - }, - "databricks-proto-first": { - "description": "Proto-first schema design for Databricks apps", - "experimental": False, - }, -} +# --------------------------------------------------------------------------- +# Sync +# --------------------------------------------------------------------------- + +def sync_assets(repo_root: Path) -> int: + """Copy shared assets from repo root into each skill directory. + + Only writes when content differs. Uses shutil.copy2 to preserve mtime + from the source so that skill updated_at timestamps stay stable. + + Returns count of files written. + """ + for asset_rel in SHARED_ASSETS: + source = repo_root / asset_rel + if not source.exists(): + raise ValueError(f"Missing shared asset '{asset_rel}' at repo root.") + + synced = 0 + for skill_dir in iter_skill_dirs(repo_root): + for asset_rel in SHARED_ASSETS: + source = repo_root / asset_rel + dest = skill_dir / asset_rel + dest.parent.mkdir(parents=True, exist_ok=True) + + if dest.exists() and dest.read_bytes() == source.read_bytes(): + continue + shutil.copy2(source, dest) + synced += 1 + + return synced + + +def check_assets_synced(repo_root: Path) -> list[str]: + """Validate that all shared assets are present and up-to-date. + + Returns a list of error messages (empty means all good). + """ + errors: list[str] = [] + for asset_rel in SHARED_ASSETS: + source = repo_root / asset_rel + if not source.exists(): + errors.append(f"Missing shared asset '{asset_rel}' at repo root.") + continue + + source_bytes = source.read_bytes() + for skill_dir in iter_skill_dirs(repo_root): + dest = skill_dir / asset_rel + if not dest.exists(): + errors.append(f"Missing '{asset_rel}' in skill '{skill_dir.name}'.") + elif dest.read_bytes() != source_bytes: + errors.append(f"Stale '{asset_rel}' in skill '{skill_dir.name}'.") + + return errors + + +# --------------------------------------------------------------------------- +# Manifest generation +# --------------------------------------------------------------------------- def generate_manifest(repo_root: Path) -> dict: """Generate manifest from skill directories.""" - # Load existing manifest to preserve base_revision fields manifest_path = repo_root / "manifest.json" existing_skills = {} if manifest_path.exists(): existing_skills = json.loads(manifest_path.read_text()).get("skills", {}) skills = {} - skills_dir = repo_root / "skills" - - for item in sorted(skills_dir.iterdir()): - if not item.is_dir(): - continue - if item.name.startswith(".") or item.name == "scripts": - continue - - skill_md = item / "SKILL.md" - if not skill_md.exists(): - continue - + for skill_dir in iter_skill_dirs(repo_root): files = sorted( - str(f.relative_to(item)) - for f in item.rglob("*") + str(f.relative_to(skill_dir)) + for f in skill_dir.rglob("*") if f.is_file() ) - if item.name not in SKILL_METADATA: - raise ValueError(f"Missing SKILL_METADATA entry for skill '{item.name}'. Add it to SKILL_METADATA dict.") + if skill_dir.name not in SKILL_METADATA: + raise ValueError( + f"Missing SKILL_METADATA entry for skill '{skill_dir.name}'. " + "Add it to SKILL_METADATA dict." + ) - metadata = SKILL_METADATA[item.name] + openai_yaml = skill_dir / "agents" / "openai.yaml" + if not openai_yaml.exists(): + raise ValueError( + f"Missing agents/openai.yaml in skill '{skill_dir.name}'. " + "Each skill must include Codex marketplace metadata." + ) + + metadata = SKILL_METADATA[skill_dir.name] skill_entry = { - "version": extract_version_from_skill(item), + "version": extract_version_from_skill(skill_dir), "description": metadata.get("description", ""), "experimental": metadata.get("experimental", False), - "updated_at": get_skill_updated_at(item), + "updated_at": get_skill_updated_at(skill_dir), "files": files, } if metadata.get("min_cli_version"): skill_entry["min_cli_version"] = metadata["min_cli_version"] - # Preserve base_revision from existing manifest - existing = existing_skills.get(item.name, {}) + existing = existing_skills.get(skill_dir.name, {}) if "base_revision" in existing: skill_entry["base_revision"] = existing["base_revision"] - skills[item.name] = skill_entry + skills[skill_dir.name] = skill_entry return { "version": "2", @@ -140,6 +214,10 @@ def generate_manifest(repo_root: Path) -> dict: } +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- + def normalize_manifest(manifest: dict) -> dict: """Normalize manifest for comparison by excluding volatile fields.""" normalized = manifest.copy() @@ -167,7 +245,6 @@ def validate_manifest(repo_root: Path) -> bool: current_manifest = json.loads(manifest_path.read_text()) expected_manifest = generate_manifest(repo_root) - # compare without timestamps current_normalized = normalize_manifest(current_manifest) expected_normalized = normalize_manifest(expected_manifest) @@ -179,34 +256,71 @@ def validate_manifest(repo_root: Path) -> bool: print(json.dumps(current_normalized, indent=2), file=sys.stderr) return False - print("manifest.json is up to date") return True +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + def main() -> None: - parser = argparse.ArgumentParser(description="Generate or validate manifest.json") + parser = argparse.ArgumentParser( + description="Manage skills: sync shared assets, generate manifest, validate." + ) parser.add_argument( "mode", nargs="?", default="generate", - choices=["generate", "validate"], - help="Mode: generate (creates manifest.json, default) or validate (checks if up to date)", + choices=["sync", "generate", "validate"], + help=( + "sync: copy shared assets into each skill directory. " + "generate: sync + create manifest.json (default). " + "validate: check assets and manifest are up to date." + ), ) args = parser.parse_args() repo_root = Path(__file__).parent.parent match args.mode: + case "sync": + synced = sync_assets(repo_root) + print(f"Synced {synced} asset(s)") + case "generate": + synced = sync_assets(repo_root) + print(f"Synced {synced} asset(s)") + manifest = generate_manifest(repo_root) manifest_path = repo_root / "manifest.json" manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") print(f"Generated {manifest_path}") - print(f"Found {len(manifest['skills'])} skill(s): {', '.join(manifest['skills'].keys())}") + print( + f"Found {len(manifest['skills'])} skill(s): " + f"{', '.join(manifest['skills'].keys())}" + ) case "validate": - is_valid = validate_manifest(repo_root) - sys.exit(0 if is_valid else 1) + ok = True + + asset_errors = check_assets_synced(repo_root) + if asset_errors: + print("ERROR: Shared assets are out of sync:", file=sys.stderr) + for err in asset_errors: + print(f" - {err}", file=sys.stderr) + ok = False + + if not validate_manifest(repo_root): + ok = False + + if not ok: + print( + "\nRun `python3 scripts/skills.py generate` to fix.", + file=sys.stderr, + ) + sys.exit(1) + + print("Everything is up to date.") if __name__ == "__main__": diff --git a/skills/databricks-apps/agents/openai.yaml b/skills/databricks-apps/agents/openai.yaml new file mode 100644 index 0000000..1e3827e --- /dev/null +++ b/skills/databricks-apps/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Databricks Apps" + short_description: "Apps development and deployment" + icon_small: "./assets/databricks.svg" + icon_large: "./assets/databricks.png" + brand_color: "#FF3621" + default_prompt: "Use $databricks-apps for Databricks Apps development and deployment." diff --git a/skills/databricks-apps/assets/databricks.png b/skills/databricks-apps/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/skills/databricks-apps/assets/databricks.png differ diff --git a/skills/databricks-apps/assets/databricks.svg b/skills/databricks-apps/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/skills/databricks-apps/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/databricks-core/agents/openai.yaml b/skills/databricks-core/agents/openai.yaml new file mode 100644 index 0000000..1b5f562 --- /dev/null +++ b/skills/databricks-core/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Databricks" + short_description: "CLI, auth, and data exploration" + icon_small: "./assets/databricks.svg" + icon_large: "./assets/databricks.png" + brand_color: "#FF3621" + default_prompt: "Use $databricks-core for Databricks CLI, auth, and data exploration." diff --git a/skills/databricks-core/assets/databricks.png b/skills/databricks-core/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/skills/databricks-core/assets/databricks.png differ diff --git a/skills/databricks-core/assets/databricks.svg b/skills/databricks-core/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/skills/databricks-core/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/databricks-dabs/agents/openai.yaml b/skills/databricks-dabs/agents/openai.yaml new file mode 100644 index 0000000..185f6e5 --- /dev/null +++ b/skills/databricks-dabs/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Databricks DABs" + short_description: "Declarative Automation Bundles for deploying and managing Databricks resources" + icon_small: "./assets/databricks.svg" + icon_large: "./assets/databricks.png" + brand_color: "#FF3621" + default_prompt: "Use $databricks-dabs for creating, deploying, and managing Databricks resources through Declarative Automation Bundles." diff --git a/skills/databricks-dabs/assets/databricks.png b/skills/databricks-dabs/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/skills/databricks-dabs/assets/databricks.png differ diff --git a/skills/databricks-dabs/assets/databricks.svg b/skills/databricks-dabs/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/skills/databricks-dabs/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/databricks-jobs/agents/openai.yaml b/skills/databricks-jobs/agents/openai.yaml new file mode 100644 index 0000000..9dd4b36 --- /dev/null +++ b/skills/databricks-jobs/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Databricks Jobs" + short_description: "Jobs orchestration and scheduling" + icon_small: "./assets/databricks.svg" + icon_large: "./assets/databricks.png" + brand_color: "#FF3621" + default_prompt: "Use $databricks-jobs for Databricks Jobs orchestration and scheduling." diff --git a/skills/databricks-jobs/assets/databricks.png b/skills/databricks-jobs/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/skills/databricks-jobs/assets/databricks.png differ diff --git a/skills/databricks-jobs/assets/databricks.svg b/skills/databricks-jobs/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/skills/databricks-jobs/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/databricks-lakebase/agents/openai.yaml b/skills/databricks-lakebase/agents/openai.yaml new file mode 100644 index 0000000..541b208 --- /dev/null +++ b/skills/databricks-lakebase/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Databricks Lakebase" + short_description: "Lakebase database development" + icon_small: "./assets/databricks.svg" + icon_large: "./assets/databricks.png" + brand_color: "#FF3621" + default_prompt: "Use $databricks-lakebase for Databricks Lakebase database development." diff --git a/skills/databricks-lakebase/assets/databricks.png b/skills/databricks-lakebase/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/skills/databricks-lakebase/assets/databricks.png differ diff --git a/skills/databricks-lakebase/assets/databricks.svg b/skills/databricks-lakebase/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/skills/databricks-lakebase/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/databricks-pipelines/agents/openai.yaml b/skills/databricks-pipelines/agents/openai.yaml new file mode 100644 index 0000000..a6d7111 --- /dev/null +++ b/skills/databricks-pipelines/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Databricks Pipelines" + short_description: "Pipelines for ETL and streaming" + icon_small: "./assets/databricks.svg" + icon_large: "./assets/databricks.png" + brand_color: "#FF3621" + default_prompt: "Use $databricks-pipelines for Databricks Pipelines ETL and streaming." diff --git a/skills/databricks-pipelines/assets/databricks.png b/skills/databricks-pipelines/assets/databricks.png new file mode 100644 index 0000000..263fe98 Binary files /dev/null and b/skills/databricks-pipelines/assets/databricks.png differ diff --git a/skills/databricks-pipelines/assets/databricks.svg b/skills/databricks-pipelines/assets/databricks.svg new file mode 100644 index 0000000..9d19110 --- /dev/null +++ b/skills/databricks-pipelines/assets/databricks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file