diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..4857a6b --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,54 @@ +name: Deploy GitHub Pages + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - 'marketplace/**' + - 'catalog/**' + - 'scripts/**' + - 'docs/**' + - '.github/workflows/deploy-pages.yml' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies and generate documentation + run: | + source $HOME/.cargo/env + make install + make generate + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'docs' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..fba49a4 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,26 @@ +name: Validate Catalog + +on: + pull_request: + push: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies and verify site generation + run: | + source $HOME/.cargo/env + make install + make test diff --git a/.gitignore b/.gitignore index 13842d5..ebf8e77 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,9 @@ config.local.* # Lola package manager cache .lola/ +# Serena AI assistant workspace +.serena/ + # Generated Documentation docs/data.json reports/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..35da3a2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +# AGENTS.md + +Guidance for AI coding assistants working in this repository. + +## What This Repository Is + +This is the **catalog and marketplace** for Red Hat agentic collections. It serves two purposes: + +1. **Lola marketplace** — `marketplace/rh-agentic-collection.yml` declares which skill packs are available and where to fetch them. Users install packs with `lola install -f `. +2. **agentskills.io website** — the `docs/` directory is the static site. The scripts in `scripts/` build it by cloning each pack's source repo and extracting metadata. + +**No skills are authored or stored here.** Skills live in their own source repos (e.g., [agentic-collections-skills](https://github.com/RHEcosystemAppEng/agentic-collections-skills)). This repo only aggregates and presents them. + +## Repository Structure + +``` +agentic-collections-catalog/ +├── marketplace/ +│ └── rh-agentic-collection.yml # Single source of truth for pack discovery +├── catalog/ +│ └── schema.yaml # JSON Schema for .catalog/collection.yaml files in skills repos +├── docs/ # Static site (agentskills.io) +│ ├── index.html # SPA entry point +│ ├── app.js # Rendering and search logic (XSS-safe, no innerHTML) +│ ├── styles.css # Red Hat-themed styling +│ ├── data.json # Generated — do not edit manually +│ ├── mcp.json # MCP server metadata — do not edit manually +│ └── collections/ # Generated per-pack HTML pages +└── scripts/ # Build and verification scripts +``` + +## How the Build Works + +`make generate` runs `scripts/build_website.py`, which: + +1. Reads `marketplace/rh-agentic-collection.yml` to get the list of packs +2. For each pack: `git clone` its source repo into a temp directory +3. Reads `.catalog/collection.yaml` (if present) for metadata and maturity +4. Skips packs whose catalog declares a non-GREEN maturity; absent catalog → assumed GREEN +5. Reads `skills/*/SKILL.md` frontmatter, README, and `mcps.json` from the clone +6. Writes `docs/data.json` and generates `docs/collections/.html` + +The clones are ephemeral (deleted after each build). Nothing from external repos is committed here. + +## Marketplace File + +`marketplace/rh-agentic-collection.yml` is the **only** place that controls which packs appear on the site. Each module entry carries: + +| Field | Required | Purpose | +|-------|----------|---------| +| `name` | yes | Pack identifier (used in URLs and filenames) | +| `repository` | yes | Git URL to clone | +| `path` | yes | Subdirectory within the repo (`.` for root) | +| `version` | yes | Displayed version | +| `description` | yes | Short description shown on cards | +| `title` | yes | Display name shown on cards | +| `icon` | recommended | Emoji or HTTPS URL to an image | +| `tags` | optional | Filter tags | +| `ref` | optional | 40-char commit SHA to pin; absent = main branch | + +Do not add packs to `docs/data.json` or any other file directly. Add them to the marketplace YAML. + +## Scripts + +| Script | Purpose | Invoked by | +|--------|---------|------------| +| `build_website.py` | Orchestrates the full build | `make generate` | +| `generate_pack_data.py` | Clones repos, extracts pack/skill metadata | build | +| `generate_mcp_data.py` | Extracts MCP server configs from `mcps.json` | build | +| `generate_collection_pages.py` | Renders per-pack HTML pages | build | +| `catalog_site_bundle.py` | Resolves `.catalog/` fragment `#ref` pointers | build | +| `eval_site_enrichment.py` | Attaches ABEval report summaries to skills | build | +| `pack_registry.py` | Marketplace-driven pack discovery utilities | build | +| `check_site.py` | Interactive manual verification of `data.json` | manual | +| `test_local.sh` | Automated validation (JSON, HTML, XSS, credentials) | `make test` | +| `validate_mcp_types.py` | Sanity-checks MCP server type parsing | manual | + +## Key Rules + +- **Marketplace is the single source of truth.** Do not add packs, icons, or titles anywhere else. +- **Generated files are read-only.** `docs/data.json`, `docs/mcp.json`, and `docs/collections/*.html` are rebuilt on every run — manual edits will be overwritten. +- **No skills development here.** To create or modify skills, work in the appropriate skills source repo. +- **Schema changes need coordination.** Updating `catalog/schema.yaml` requires matching changes in all skills repos' `.catalog/` directories. +- **Security.** All DOM manipulation in `app.js` uses `textContent` and `createElement` — never `innerHTML` with external data. + +## CI + +| Workflow | Trigger | What it does | +|----------|---------|-------------| +| `validate.yml` | Push / PR | Runs `make test` (→ `test_local.sh`) | +| `deploy-pages.yml` | Push to main | Runs `make generate`, deploys `docs/` to GitHub Pages | diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 6a7c3de..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,110 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What This Repository Is - -This is the **unified catalog and marketplace** for Red Hat agentic collections. It aggregates skills, MCP server configurations, and agentic packs from multiple source repositories into a single, browsable catalog served through [Lola](https://github.com/LobsterTrap/lola) and the [agentskills.io](https://agentskills.io) website. - -**This is NOT a skills development repository.** No skills are authored or maintained here. Skills live in their own source repositories (e.g., [agentic-collections-skills](https://github.com/RHEcosystemAppEng/agentic-collections-skills)). An internal process fetches content from those repos, evaluates it with its own scorecard, and assembles the catalog automatically. - -## Repository Structure - -``` -agentic-collections-catalog/ -├── marketplace/ # Lola marketplace definition -│ └── rh-agentic-collection.yml # Module registry — what users install via `lola install -f` -├── catalog/ # Collection schema for validation -│ └── schema.yaml -├── docs/ # Website source (agentskills.io) -│ ├── index.html # Landing page -│ ├── app.js # Application logic -│ ├── styles.css # Styling -│ ├── data.json # Generated catalog data (skills, packs, MCPs) -│ ├── plugins.json # Plugin registry -│ ├── mcp.json # MCP server metadata -│ └── collections/ # Generated per-pack HTML pages -├── scripts/ # Catalog build and maintenance scripts -├── COLLECTION_SPEC.md # Specification for .catalog/ structure -├── README.md -└── LICENSE -``` - -## How the Catalog Works - -### Data Flow - -``` -Skills repos (multiple) Internal process This repo -┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ -│ agentic-collections-│ │ Fetch skills repos │ │ marketplace/ │ -│ skills/ │────>│ Evaluate & score │────>│ docs/ (website) │ -│ rh-sre/ │ │ Build catalog data │ │ data.json │ -│ ocp-admin/ │ │ Generate website │ │ plugins.json │ -│ ... │ └──────────────────────┘ └─────────────────────┘ -├─────────────────────┤ -│ other-skills-repo/ │────> (same process) -└─────────────────────┘ -``` - -Each skills repo contains packs with `.catalog/` metadata that describes its content. The internal process reads that metadata, applies its own evaluation and scoring, and publishes the results here. - -### Marketplace (Lola) - -`marketplace/rh-agentic-collection.yml` is the Lola module registry. Users consume it with: - -```bash -lola market add rh-agentic-collections -lola install -f rh-sre -``` - -This file defines which packs are available, their versions, and where to fetch them. - -### Website (agentskills.io) - -The `docs/` directory contains the static site served at agentskills.io. Key files: - -- `data.json` — Generated catalog data aggregating all packs, skills, and MCP servers -- `plugins.json` — Plugin registry for marketplace discovery -- `mcp.json` — MCP server metadata and tool descriptions -- `collections/*.html` — Per-pack detail pages - -### Collection Schema - -- `catalog/schema.yaml` — JSON Schema that defines the structure of `.catalog/collection.yaml` files in skills repos -- `COLLECTION_SPEC.md` — Human-readable specification with rules for catalog metadata (inline vs fragment, path conventions) - -## Scripts - -Scripts in `scripts/` support catalog build and maintenance: - -| Script | Purpose | -|--------|---------| -| `build_website.py` | Generate `docs/data.json` from pack data | -| `generate_pack_data.py` | Extract pack metadata for the website | -| `generate_mcp_data.py` | Extract MCP server data for the website | -| `generate_collection_pages.py` | Generate per-pack HTML pages | -| `catalog_site_bundle.py` | Resolve `.catalog/` fragment refs for site embedding | -| `check_site.py` | Interactive site summary and manual testing checklist | -| `test_local.sh` | Automated site verification (data.json, HTML, XSS, credentials) | -| `validate_mcp_types.py` | Validate MCP server type parsing (command vs HTTP) | -| `eval_site_enrichment.py` | Enrich catalog with evaluation data | -| `pack_registry.py` | Discover packs from marketplace and plugins.json | - -## Key Considerations - -### No CI in This Repo - -This repository has no GitHub Actions workflows. The internal process that builds the catalog handles all validation, evaluation, and deployment. - -### No Skills Development - -If you need to create or modify skills, work in the appropriate skills source repository, not here. This repo only contains the infrastructure to aggregate and present skills. - -### Catalog Data is Generated - -Files like `docs/data.json`, `docs/collections/*.html`, and parts of `plugins.json` are generated artifacts. They are rebuilt by the internal process and should not be edited manually. - -### Schema Changes - -If the structure of `.catalog/collection.yaml` needs to change, update `catalog/schema.yaml` and `COLLECTION_SPEC.md` here, then coordinate with skills repos to update their `.catalog/` directories accordingly. diff --git a/COLLECTION_SPEC.md b/COLLECTION_SPEC.md deleted file mode 100644 index 0597879..0000000 --- a/COLLECTION_SPEC.md +++ /dev/null @@ -1,80 +0,0 @@ -# Collection catalog specification - -This repository uses a **pack-local collection catalog**: curated metadata and summaries live under **`/.catalog/`** (YAML as the source of record, JSON as a deterministic mirror for consumers that prefer it). **Golden sources** are pack `SKILL.md` files, `README.md`, `AGENTS.md`, and [`marketplace/rh-agentic-collection.yml`](marketplace/rh-agentic-collection.yml). Catalog files **describe** the collection for tooling and documentation; they are **authored** primarily via the [**create-collection**](.claude/skills/create-collection/SKILL.md) skill (assistant + maintainer + PR review) and must not overwrite READMEs or marketplace YAML. - -**Machine validation:** [`catalog/schema.yaml`](catalog/schema.yaml) (JSON Schema expressed in YAML) and [`scripts/validate_collection_compliance.py`](scripts/validate_collection_compliance.py). These checks are primarily structural and cannot fully evaluate semantic parity with golden-source docs. **Required review gate:** run an AI semantic alignment review (via [`.claude/skills/collection-compliance/SKILL.md`](.claude/skills/collection-compliance/SKILL.md)) to confirm catalog metadata/fragments remain aligned with `SKILL.md`, `README.md`, `AGENTS.md`, and marketplace module metadata. **Pack list (registry):** union of Lola marketplace `modules[].path` and keys of [`docs/plugins.json`](docs/plugins.json), limited to directories that exist on disk — see [`scripts/pack_registry.py`](scripts/pack_registry.py). **GitHub Pages / bundled catalog listing:** only packs whose **`maturity`** is **`GREEN`** are included in site generation (`docs/data.json`, collection pages, MCP bundle for docs); **`ORANGE`** means the pack remains installable from source/Lola but is not promoted on the public catalog until reviewers raise maturity. - -### `maturity` (required) - -| Value | Meaning | -|-------|---------| -| **`GREEN`** | Listed on the GitHub Pages agentic collections catalog and included in generated `docs/data.json` / static collection pages. | -| **`ORANGE`** | Collection metadata is maintained under `.catalog/` for validation and future promotion; excluded from the public catalog surface until explicitly moved to **`GREEN`**. | - -Set in **`collection.yaml`** as a plain string (`maturity: GREEN` or `maturity: ORANGE`). Enforcement: [`scripts/pack_registry.py`](scripts/pack_registry.py) (`get_docs_pack_dirs()` filters on **`GREEN`**). - -## Layout - -| Path | Purpose | -|------|---------| -| `/.catalog/collection.yaml` | Canonical catalog document (YAML) | -| `/.catalog/collection.json` | Deterministic JSON mirror of YAML (regenerate with `make catalog-mirror-json`; CI fails on drift) | -| `/.catalog/*.md` | Optional prose fragments, **siblings of** `collection.yaml`. Reference them with **`#.md`** (quoted in YAML) **on the same field** as inline text would use—for example `documentation_section: '#docs.md'` or `deploy_and_use: '#install.md'`. If inline text in `collection.yaml` for a monitored field exceeds **`CATALOG_INLINE_CHAR_LIMIT`** (500 Unicode code points; see `scripts/collection_validate_lib.py`), move prose here and set **that same field** to the ref. | - -**Multiline YAML in `collection.yaml`:** For `contents.description`, each **`summary_markdown`**, and each **`sample_workflows[].workflow`**, authors should use YAML **literal block scalars** (`field: |`) with indented lines—see step 4 of the [**create-collection**](.claude/skills/create-collection/SKILL.md) skill (reference **`rh-sre`** / **`rh-developer`** `.catalog/collection.yaml`). CI does not assert this style; it is a maintainability convention. - -### External references (`#…md` on the same field) - -- **Path rule:** refs are **siblings of** `collection.yaml` inside **`/.catalog/`**. Write **`#install.md`**, not `#.catalog/install.md` (omit the `.catalog/` segment in the string). -- **One field, two shapes:** for **`deploy_and_use`**, **`documentation_section`**, **`mcp_section`**, and **`security_model`**, the value is either **inline** markdown or a **one-line** fragment ref **`#.md`**. Do **not** use separate `documentation_section_file` / `mcp_section_file` / `security_model_file` keys. -- **Monitored inline length:** for **`summary`**, **`documentation_section`**, **`mcp_section`**, and **`security_model`**, if the value is **inline** (not a `#…md` ref) and longer than **500 Unicode code points**, move the prose to a fragment file and set **that same key** to **`#.md`** (one line only). For **`deploy_and_use`**, the same limit applies when it is **inline** markdown (not a one-line ref). -- **`deploy_and_use` (two flavors):** (1) **Inline** — markdown in YAML, ≤ **500** code points unless you externalize. (2) **File ref** — one line only: **`#.md`** next to `collection.yaml`. CI resolves the file under **`/.catalog/`**. -- **Fragment ref values:** must start with **`#`** (e.g. `#documentation_section.md`). Legacy `#.catalog/…` is accepted and normalized to a sibling path. - -### `deploy_and_use` content (install + env + MCP) - -Consumers read **`deploy_and_use`** (resolved from inline YAML or from **`#deploy_and_use.md`**) for marketplace install steps and operator setup. **CI does not yet cross-check `mcps.json` or README install parity;** reviewers enforce this section with the [**create-collection**](.claude/skills/create-collection/SKILL.md) and [**collection-compliance**](.claude/skills/collection-compliance/SKILL.md) workflows. - -**When `/mcps.json` defines one or more MCP servers**, the prose behind **`deploy_and_use`** (inline or fragment) **must** cover, in substance: - -1. **Prerequisites** — Cluster/product access, CLIs, operators, or accounts needed before install, aligned with **`README.md`** / **`AGENTS.md`**. -2. **Environment setup** — Example **`export VAR=...`** (or host env equivalent) for **every** environment variable **name** referenced in **`mcps.json`** via **`${VAR}`** in `command`, `args`, or `env`. Variable **names** must match **`mcps.json`** exactly. State that secrets are never committed and must not be echoed in assistant output. -3. **MCP configuration** — Server definitions live in **`mcps.json`** at the pack root; use **`${...}`** placeholders only; briefly note isolation/network/credentials posture if it affects the user (see existing pack fragments). -4. **Installation** — At minimum **Lola** (`lola install -f `) and the module **`path:`** in **`marketplace/rh-agentic-collection.yml`**. If **`marketplaces`** in the catalog includes **Claude Code** or **Cursor**, include install notes for those hosts (copy the pattern from **`rh-sre/.catalog/deploy_and_use.md`**). - -**Layout recommendation:** Prefer **`deploy_and_use: '#deploy_and_use.md'`** and a sibling **`deploy_and_use.md`** so `collection.yaml` stays short. Use the **same section order** as **`rh-sre/.catalog/deploy_and_use.md`**: Prerequisites → Environment setup → Installation (Lola, then Claude Code / Cursor as applicable) → MCP configuration. For a kubeconfig-only MCP story, see **`rh-virt/.catalog/deploy_and_use.md`**. - -**Packs with no MCP servers** (or a trivial single-token env story): a compact **`deploy_and_use: |`** under **500** Unicode code points is acceptable if it still states Lola/repo install and defers detail to **`README.md`** where needed. - -**Plugin / install IDs:** default `id` equals the pack directory name. Overrides: **`rh-virt`** → `openshift-virtualization`; **`ocp-admin`** → `openshift-administration`. - -## Source precedence (pack-local) - -When multiple sources could supply the same logical field: - -1. **`skills/*/SKILL.md`** (per-skill `name`, `description`, body for `summary_markdown` hints) -2. **`/README.md`** -3. **`/AGENTS.md`** (intent routing for decision-guide hints, personas) - -**Lola marketplace:** the module whose `path` equals `` supplies `version`, module `name`, module `description`, and `tags` for listing-oriented fields. **Never** write back to marketplace YAML from automation or the create-collection workflow. - -## Provenance banners - -| Artifact | Banner | -|----------|--------| -| `collection.yaml` | Leading `#` comment block: must mention **create-collection** workflow and **golden sources** (SKILL, README, AGENTS, marketplace). | -| `.catalog/*.md` fragments | Leading HTML `` with the same intent. | -| `collection.json` | **No** embedded banner; **CI** regenerates from YAML and fails if the committed file differs. | - -## Orchestration vs regular skills - -**Primary:** maintainer / assistant judgment while following **create-collection**. **Optional:** `metadata.collection.role: orchestration` in `SKILL.md` frontmatter for clearer compliance checks — not required on every skill. - -## Completeness and CI - -All **required** schema fields must be present on merge to `main` (no empty placeholders, no `TODO:` / `TBD` in `sample_workflows.workflow`). Author field values so they stay aligned with **`AGENTS.md`**, **`mcps.json`**, and **`README.md`** where those sources define behavior; fragment refs follow [External references](#external-references-md-on-the-same-field). CI runs **`make validate`** (includes structure + **collection compliance**). - -## Related - -- [`SKILL_DESIGN_PRINCIPLES.md`](SKILL_DESIGN_PRINCIPLES.md) — skill content (Tier 2) -- [`.claude/skills/collection-compliance/SKILL.md`](.claude/skills/collection-compliance/SKILL.md) — validation workflow diff --git a/OPEN_ITEMS.md b/OPEN_ITEMS.md new file mode 100644 index 0000000..93aee2b --- /dev/null +++ b/OPEN_ITEMS.md @@ -0,0 +1,12 @@ +## Open Items +### Marketplace configuration +* How to collect `icon` field from Compass registration +* How to collect `title` field from Compass registration +* Replace [mcp.json](./docs/mcp.json) + +### External contributions +* How to replace collection pack info in absemce of collection.yml? + * README + * License + * List of skills + * Installation \ No newline at end of file diff --git a/README.md b/README.md index fd0ace4..39880b7 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,7 @@ agentic-collections-catalog/ │ ├── plugins.json # Plugin registry │ ├── mcp.json # MCP server metadata │ └── collections/ # Generated per-pack HTML pages -├── scripts/ # Catalog build scripts -├── COLLECTION_SPEC.md # Specification for .catalog/ structure +├── scripts/ # Catalog build and validation scripts └── Makefile ``` diff --git a/catalog/schema.yaml b/catalog/schema.yaml deleted file mode 100644 index f46fa57..0000000 --- a/catalog/schema.yaml +++ /dev/null @@ -1,224 +0,0 @@ -# Pack-local collection catalog — JSON Schema (Draft 2020-12), YAML encoding. -# Field semantics and authoring: COLLECTION_SPEC.md. Instance files: /.catalog/collection.yaml -# Fragments: sibling *.md of collection.yaml; reference as #name.md (quote in YAML). Inline monitored fields -# >500 code points must use a fragment; see COLLECTION_SPEC.md. Resolved by scripts/collection_validate_lib.py. - -$schema: https://json-schema.org/draft/2020-12/schema -$id: https://github.com/RHEcosystemAppEng/agentic-collections/catalog/schema.yaml -title: Agentic collection catalog -type: object -additionalProperties: true -required: -- id -- name -- provider -- version -- categories -- personas -- marketplaces -- maturity -- description -- summary -- contents -- deploy_and_use -- mcp_section -- sample_workflows -- resources -properties: - id: - type: string - minLength: 1 - name: - type: string - minLength: 1 - provider: - type: string - minLength: 1 - version: - type: string - pattern: ^[0-9]+\.[0-9]+\.[0-9]+ - categories: - type: array - minItems: 1 - items: - type: string - minLength: 1 - personas: - type: array - minItems: 1 - items: - type: string - minLength: 1 - marketplaces: - type: array - minItems: 1 - items: - type: string - minLength: 1 - support_level: - type: string - maturity: - type: string - description: > - Publication readiness for the GitHub Pages catalog and bundled site data. GREEN collections are listed; - ORANGE are maintained in-repo but excluded from the public catalog until promoted. - enum: - - GREEN - - ORANGE - description: - type: string - minLength: 1 - summary: - type: string - minLength: 1 - contents: - type: object - additionalProperties: true - required: - - description - - skills - - orchestration_skills - - skills_decision_guide - properties: - description: - type: string - minLength: 1 - skills: - type: array - items: - $ref: '#/$defs/catalogSkill' - orchestration_skills: - type: array - items: - $ref: '#/$defs/catalogSkill' - skills_decision_guide: - type: array - items: - $ref: '#/$defs/decisionGuideRow' - deploy_and_use: - type: string - minLength: 1 - description: > - Inline markdown (≤500 code points unless externalized) or a one-line fragment ref - '#filename.md' sibling under .catalog/. - documentation_section: - type: string - minLength: 1 - description: > - How documentation is organized; inline markdown or one-line '#fragment.md' under .catalog/ - (same convention as deploy_and_use). - mcp_section: - type: string - minLength: 1 - description: > - MCP usage summary; inline or '#fragment.md' under .catalog/. - security_model: - type: string - minLength: 1 - description: > - Security posture narrative; inline or '#fragment.md' under .catalog/. - sample_workflows: - type: array - minItems: 1 - items: - $ref: '#/$defs/sampleWorkflow' - resources: - type: array - minItems: 1 - items: - $ref: '#/$defs/resource' - legal_resources: - type: object - author: - type: object - homepage: - type: string - repository: - type: string - license: - type: string - keywords: - type: array - items: - type: string - documentation_section: - description: > - Inline markdown, or a one-line sibling fragment ref `#name.md` under .catalog/ (same rules as deploy_and_use). - type: string - minLength: 1 - mcp_section: - description: > - Inline markdown, or a one-line sibling fragment ref `#name.md` under .catalog/ (same rules as deploy_and_use). - type: string - minLength: 1 - security_model: - description: > - Inline markdown, or a one-line sibling fragment ref `#name.md` under .catalog/ (same rules as deploy_and_use). - type: string - minLength: 1 -$defs: - catalogSkill: - type: object - required: - - name - - description - - summary_markdown - additionalProperties: true - properties: - name: - type: string - minLength: 1 - description: - type: string - minLength: 1 - summary_markdown: - type: string - minLength: 1 - decisionGuideRow: - type: object - required: - - user_request - - skill_to_use - - reason - additionalProperties: false - properties: - user_request: - type: string - minLength: 1 - skill_to_use: - type: string - minLength: 1 - reason: - type: string - minLength: 1 - sampleWorkflow: - type: object - required: - - name - - workflow - additionalProperties: false - properties: - name: - type: string - minLength: 1 - workflow: - type: string - minLength: 10 - resource: - type: object - required: - - title - - description - - url - additionalProperties: true - properties: - title: - type: string - minLength: 1 - description: - type: string - url: - type: string - minLength: 1 - embedded_doc: - type: string diff --git a/docs/README.md b/docs/README.md index 57292f7..87ea479 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,8 +12,6 @@ This directory contains the generated GitHub Pages site for agentic-collections. ## Local Development -From the repository root, install git hooks once (`make install` then `scripts/install-hooks.sh`) so commits run [pre-commit](https://pre-commit.com/) (gitleaks + scoped `make validate` / skill design checks); see the main [README.md](../README.md#security). - Run locally: ```bash python -m http.server 8000 @@ -36,28 +34,18 @@ When present under the repository root, **`eval///report. The **`name`** of each skill in **`collection.yaml`** must match the **`skills//`** directory and the **`eval///`** folder segment so lookups resolve correctly. -## Manual Updates - -To manually update the site: -1. Modify pack configurations (`mcps.json`, skills, agents; optional `.claude-plugin/plugin.json`) and per-pack **`.catalog/collection.yaml`** when catalog metadata changes (see [COLLECTION_SPEC.md](../COLLECTION_SPEC.md) and [catalog/schema.yaml](../catalog/schema.yaml); use the **create-collection** / **collection-compliance** repo skills). -2. If you edit `collection.yaml`, refresh mirrors: `make catalog-mirror-json` from the repository root. -3. Run `make generate` from the root directory (runs `make validate` first when using `make test`). -4. Commit the updated `docs/data.json` and any `.catalog/` files. -5. Push to trigger GitHub Pages deployment. - ## Validation for Docs Metadata -When changing docs presentation metadata (`styles.css`, `app.js`, `icons.json`, `plugins.json`, `mcp.json`): +When changing docs presentation metadata (`styles.css`, `app.js`, `mcp.json`): -1. Prefer canonical tokens from `styles.css` and `style-tokens.json` over hardcoded values. -2. Ensure every in-scope pack has icon/title mappings in `icons.json` and `plugins.json`. -3. Run validation checks: +1. Prefer canonical tokens from `styles.css` over hardcoded values. +2. Run validation checks: ```bash make validate ``` -4. Review validation output before merge. +3. Review validation output before merge. ## Security diff --git a/docs/app.js b/docs/app.js index 3db5e71..702fd2a 100644 --- a/docs/app.js +++ b/docs/app.js @@ -175,6 +175,19 @@ function formatMetric(value, digits = 2) { return typeof value === 'number' && Number.isFinite(value) ? value.toFixed(digits) : 'N/A'; } +function createIconElement(icon, className) { + const isUrl = icon.startsWith('http'); + const el = document.createElement(isUrl ? 'img' : 'span'); + if (className) el.className = className; + if (isUrl) { + el.src = icon; + el.alt = ''; + } else { + el.textContent = icon; + } + return el; +} + function createPackCard(pack) { const NS_SVG = 'http://www.w3.org/2000/svg'; const makeSvg = () => { @@ -274,10 +287,7 @@ function createPackCard(pack) { // Custom icon (if available) if (pack.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'card-icon'; - customIcon.textContent = pack.icon; - h3.appendChild(customIcon); + h3.appendChild(createIconElement(pack.icon, 'card-icon')); } const titleText = document.createElement('span'); @@ -288,10 +298,10 @@ function createPackCard(pack) { const badges = document.createElement('div'); badges.className = 'pack-card-badges'; - if (es && es.catalog_skill_count > 0) { + { const badge = document.createElement('span'); badge.className = 'pack-eval-badge'; - if (es.evaluated_count === 0) { + if (!es || es.evaluated_count === 0) { badge.classList.add('is-none'); badge.textContent = 'NOT EVALUATED'; } else if (es.failed_count === 0) { @@ -306,12 +316,6 @@ function createPackCard(pack) { } badges.appendChild(badge); } - if (pack.source === 'federated') { - const fedBadge = document.createElement('span'); - fedBadge.className = 'pack-eval-badge is-federated'; - fedBadge.textContent = 'FEDERATED'; - badges.appendChild(fedBadge); - } if (badges.childNodes.length > 0) { headerRow.appendChild(badges); } @@ -319,11 +323,8 @@ function createPackCard(pack) { const meta = document.createElement('p'); meta.className = 'pack-meta'; - if (pack.source === 'federated') { - meta.textContent = `External · v${pack.plugin.version || '0.0.0'} · ${pack.ref || ''}`; - } else { - meta.textContent = `By Red Hat · v${pack.plugin.version || '0.0.0'}`; - } + const author = pack.plugin.author?.name || 'Red Hat'; + meta.textContent = `By ${author} · v${pack.plugin.version || '0.0.0'}`; div.appendChild(meta); // Description (prefer collection catalog metadata over plugin fallback text) @@ -561,11 +562,7 @@ function createMCPCard(server) { // Custom icon (if available) if (server.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'card-icon'; - customIcon.textContent = server.icon; - customIcon.style.fontSize = '1.2rem'; - h3.appendChild(customIcon); + h3.appendChild(createIconElement(server.icon, 'card-icon')); } const titleText = document.createElement('span'); @@ -1144,11 +1141,7 @@ lola install -f ${moduleName}`; // Custom icon (if available) if (server.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'item-icon'; - customIcon.textContent = server.icon; - customIcon.style.fontSize = '0.9rem'; - nameGroup.appendChild(customIcon); + nameGroup.appendChild(createIconElement(server.icon, 'item-icon')); } const nameCode = document.createElement('code'); @@ -1254,11 +1247,7 @@ function showMCPDetails(serverName, packName) { // Custom icon (if available) if (server.icon) { - const customIcon = document.createElement('span'); - customIcon.className = 'card-icon'; - customIcon.textContent = server.icon; - customIcon.style.fontSize = '1.8rem'; - h2.appendChild(customIcon); + h2.appendChild(createIconElement(server.icon, 'card-icon')); } const titleText = document.createElement('span'); @@ -2615,9 +2604,7 @@ function buildCollectionAgentsPanel(panel, pack, c) { const titleRow = document.createElement('div'); titleRow.className = 'collection-mcp-card-title'; if (server.icon) { - const ic = document.createElement('span'); - ic.textContent = server.icon; - titleRow.appendChild(ic); + titleRow.appendChild(createIconElement(server.icon, '')); } const t = document.createElement('span'); t.textContent = server.title || server.name; diff --git a/docs/icons.json b/docs/icons.json deleted file mode 100644 index 81a9f25..0000000 --- a/docs/icons.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "packs": { - "rh-sre": "🔧", - "rh-developer": "💻", - "ocp-admin": "☸️", - "rh-virt": "🖥️", - "rh-ai-engineer": "🤖", - "rh-automation": "⚙️" - }, - "mcp_servers": { - "lightspeed-mcp": "💡", - "aap-mcp-job-management": "⚙️", - "aap-mcp-inventory-management": "📋", - "openshift": "☸️", - "openshift-virtualization": "🖥️", - "podman": "🐳", - "github": "🐙", - "ai-observability": "🔬" - } -} diff --git a/docs/plugins.json b/docs/plugins.json deleted file mode 100644 index c503ffa..0000000 --- a/docs/plugins.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "rh-sre": { - "title": "Agentic skill pack for Security Reliability Engineers" - }, - "rh-virt": { - "title": "Agentic skill pack for Red Hat OpenShift Virtualization" - }, - "rh-developer": { - "title": "Agentic skill pack for Software Developers" - }, - "ocp-admin": { - "title": "Agentic skill pack for Red Hat OpenShift" - }, - "rh-ai-engineer": { - "title": "Agentic skill pack for OpenShift AI Engineers" - }, - "rh-automation": { - "title": "Agentic skill pack for Automation Engineers" - }, - "rh-basic": { - "title": "Agentic skill pack for Red Hat customers" - } -} \ No newline at end of file diff --git a/docs/style-tokens.json b/docs/style-tokens.json deleted file mode 100644 index 524517b..0000000 --- a/docs/style-tokens.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "colors": { - "primary": "var(--primary)", - "primaryDark": "var(--primary-dark)", - "primaryLight": "var(--primary-light)", - "textPrimary": "var(--text-primary)", - "textSecondary": "var(--text-secondary)", - "textMuted": "var(--text-muted)" - }, - "sizes": { - "cardIcon": "1.2rem", - "itemIcon": "1rem" - }, - "usage": { - "stylesCss": "docs/styles.css", - "appJs": "docs/app.js", - "iconsJson": "docs/icons.json", - "pluginsJson": "docs/plugins.json", - "mcpJson": "docs/mcp.json" - } -} diff --git a/docs/styles.css b/docs/styles.css index ed7be59..81b9f46 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1312,6 +1312,14 @@ footer a:hover { font-size: 1rem; } +img.card-icon, +img.item-icon { + height: 1em; + width: auto; + vertical-align: middle; + object-fit: contain; +} + /* Per-collection catalog page (#collection/) */ .collection-page { max-width: 920px; diff --git a/marketplace/rh-agentic-collection.yml b/marketplace/rh-agentic-collection.yml index 50863e8..b8daa02 100644 --- a/marketplace/rh-agentic-collection.yml +++ b/marketplace/rh-agentic-collection.yml @@ -1,14 +1,16 @@ name: "redhat-agentic-collections" description: "Validated Agentic Collections from Red Hat Ecosystem Engineering" -url: "https://github.com/RHEcosystemAppEng/agentic-collections" +url: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" version: "0.1.0" modules: - name: "ocp-admin" + title: "Agentic skill pack for Red Hat OpenShift" description: "Automation capabilities for OpenShift Container Platform cluster management, workload orchestration, security policies, and operational tasks." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "ocp-admin" + icon: "☸️" tags: - "red-hat" - "openshift" @@ -23,10 +25,12 @@ modules: - "vulnerability-management" - name: "rh-developer" + title: "Agentic skill pack for Software Developers" description: "Plugins for building and deploying applications on Red Hat platforms." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-developer" + icon: "💻" tags: - "developer" - "openshift" @@ -38,10 +42,12 @@ modules: - "podman" - name: "rh-sre" + title: "Agentic skill pack for Security Reliability Engineers" description: "Site reliability engineering tools and automation for managing Red Hat platforms and infrastructure." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-sre" + icon: "🔧" tags: - "sre" - "red-hat" @@ -50,10 +56,12 @@ modules: - "automation" - name: "rh-ai-engineer" + title: "Agentic skill pack for OpenShift AI Engineers" description: "Red Hat OpenShift AI Engineering Agentic Collection" version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-ai-engineer" + icon: "🤖" tags: - "ai-engineer" - "openshift-ai" @@ -66,10 +74,12 @@ modules: - "gpu" - name: "rh-virt" + title: "Agentic skill pack for Red Hat OpenShift Virtualization" description: "Virtual machine management and automation for OpenShift Virtualization and KubeVirt workloads." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-virt" + icon: "🖥️" tags: - "red-hat" - "kubevirt" @@ -78,10 +88,12 @@ modules: - "virt" - name: "rh-basic" + title: "Agentic skill pack for Red Hat customers" description: "Essential Red Hat skills for IT professionals: CVE explanation, diagnostics, product lifecycle, RHEL patch planning, and support severity guidance." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-basic" + icon: "https://static.redhat.com/libs/redhat/brand-assets/latest/corp/logo--hat-only.svg" tags: - "red-hat" - "cve" @@ -92,10 +104,12 @@ modules: - "patching" - name: "rh-automation" + title: "Agentic skill pack for Automation Engineers" description: "Ansible Automation Platform governance, execution safety, and forensic troubleshooting tools for Red Hat automation engineers." version: "0.1.0" - repository: "https://github.com/RHEcosystemAppEng/agentic-collections" + repository: "https://github.com/RHEcosystemAppEng/agentic-collections-skills" path: "rh-automation" + icon: "⚙️" tags: - "ansible" - "aap" @@ -103,26 +117,4 @@ modules: - "red-hat" - "governance" - "troubleshooting" - - "execution" - - # Federated modules — complete Lola packs hosted in external repositories. - # Add them as regular module entries with a different repository URL. - # The build identifies federated packs by comparing the repository field - # against this repo's URL. Extra field (ref) is a project extension: required - # here for CI/catalog pinning (40-character commit SHA). Lola ignores ref at - # install time until https://github.com/LobsterTrap/lola/issues/180 — installs - # use the repository default branch. Lola installs the full pack at path. - # To request federation, open a PR adding the module entry with the "federation" label. - # See FEDERATION_REQUEST_GUIDE.md. - # - # Example: - # - name: "partner-network-tools" - # description: "Network troubleshooting skills for SDN and OVN diagnostics" - # version: "1.2.0" - # repository: "https://github.com/partner-org/network-skills" - # ref: "a1b2c3d4e5f6789012345678901234567890abcd" # required: 40-character commit SHA - # path: "." # Lola pack root; all skills/ here are installed - # tags: - # - "networking" - # - "sdn" - # - "troubleshooting" + - "execution" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..91ae0e4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "agentic-collections-catalog" +version = "0.1.0" +description = "Unified catalog and marketplace for Red Hat agentic collections" +requires-python = ">=3.11" +dependencies = [ + "PyYAML>=6.0", + "markdown>=3.6", +] diff --git a/scripts/build_website.py b/scripts/build_website.py index b8c8e95..03106d3 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -7,7 +7,7 @@ import sys from datetime import datetime, timezone from pathlib import Path -from typing import Dict, Any + # Import our data generators from catalog_site_bundle import bundle_catalog_for_site @@ -17,27 +17,6 @@ from generate_mcp_data import generate_mcp_data -def load_icons() -> Dict[str, Dict[str, str]]: - """ - Load icon mappings from docs/icons.json. - - Returns: - Dictionary with 'packs' and 'mcp_servers' icon mappings - """ - icons_file = Path('docs/icons.json') - - if not icons_file.exists(): - print("⚠️ Warning: docs/icons.json not found, icons will not be loaded") - return {'packs': {}, 'mcp_servers': {}} - - try: - with open(icons_file, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"⚠️ Warning: Failed to load docs/icons.json: {e}") - return {'packs': {}, 'mcp_servers': {}} - - def build_website(): """ Generate the complete website data file. @@ -45,27 +24,22 @@ def build_website(): print("🔨 Building documentation website...") print() - # Load icons - print("🎨 Loading icons...") - icons = load_icons() - print() - # Generate pack data print("📦 Parsing agentic collections...") pack_data = generate_pack_data() - + root = Path(__file__).resolve().parent.parent - # Merge pack icons and optional resolved collection catalog (for Pages UI) + # Resolve collection catalog for packs that don't have it from the clone phase for pack in pack_data: - pack_name = pack['name'] - pack['icon'] = icons['packs'].get(pack_name, '') - catalog_dir = pack.get('catalog_dir', pack_name) - cat_bundle, cat_warns = bundle_catalog_for_site(catalog_dir, root) - for w in cat_warns: - print(f"⚠️ {w}") - if cat_bundle is not None: - pack['collection'] = cat_bundle + if 'collection' not in pack: + pack_name = pack['name'] + catalog_dir = pack.get('catalog_dir', pack_name) + cat_bundle, cat_warns = bundle_catalog_for_site(catalog_dir, root) + for w in cat_warns: + print(f"⚠️ {w}") + if cat_bundle is not None: + pack['collection'] = cat_bundle # Keep pack cards deterministic and alphabetically ordered. pack_data = sorted(pack_data, key=lambda p: p['name']) @@ -76,13 +50,7 @@ def build_website(): # Generate MCP server data print("🔌 Parsing MCP servers...") - mcp_data = generate_mcp_data() - - # Merge MCP server icons - for server in mcp_data: - server_name = server['name'] - server['icon'] = icons['mcp_servers'].get(server_name, '') - + mcp_data = generate_mcp_data(pack_data) print() # Generate static collection pages (fork-compatible UX) diff --git a/scripts/generate_collection_pages.py b/scripts/generate_collection_pages.py index 10cf0a0..afc5541 100644 --- a/scripts/generate_collection_pages.py +++ b/scripts/generate_collection_pages.py @@ -15,8 +15,34 @@ from typing import Any, Dict, List import markdown +import yaml REPO_ROOT = Path(__file__).resolve().parent.parent +_MARKETPLACE_PATH = REPO_ROOT / "marketplace" / "rh-agentic-collection.yml" +def _load_marketplace_modules() -> Dict[str, Any]: + if not _MARKETPLACE_PATH.exists(): + return {} + with open(_MARKETPLACE_PATH, encoding="utf-8") as f: + data = yaml.safe_load(f) or {} + return {mod["name"]: mod for mod in (data.get("modules") or []) if "name" in mod} + + +_MARKETPLACE_MODULES: Dict[str, Any] = _load_marketplace_modules() + + +def _pack_blob_base(pack: Dict[str, Any]) -> str: + """Return the GitHub blob URL base (repo/blob/ref/path) for a pack.""" + name = pack.get("name", "") + mod = _MARKETPLACE_MODULES.get(name, {}) + repo = (pack.get("repository") or mod.get("repository") or "").rstrip("/") + if not repo: + raise ValueError(f"Pack '{name}' has no repository in pack data or marketplace") + ref = pack.get("ref") or "main" + path = (mod.get("path") if mod else None) or name + path = path.strip("/") + if path and path != ".": + return f"{repo}/blob/{ref}/{path}" + return f"{repo}/blob/{ref}" def md_to_html(text: Any) -> str: @@ -384,7 +410,7 @@ def _metric(val: Any, digits: int = 2) -> str: return "".join(parts) -def _render_skills_list(skills: List[Dict[str, Any]], pack_name: str, list_tag: str | None = None) -> str: +def _render_skills_list(skills: List[Dict[str, Any]], blob_base: str, list_tag: str | None = None) -> str: items: List[str] = [] for skill in skills: if not isinstance(skill, dict): @@ -392,7 +418,7 @@ def _render_skills_list(skills: List[Dict[str, Any]], pack_name: str, list_tag: name = str(skill.get("name", "")) desc = html.escape(str(skill.get("description", ""))) summary = md_to_html(skill.get("summary_markdown", "")) - skill_path = f"https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/{pack_name}/skills/{name}/SKILL.md" + skill_path = f"{blob_base}/skills/{name}/SKILL.md" heading = f"
/{html.escape(name)} - {desc}" if list_tag: heading += f' {html.escape(list_tag)}' @@ -442,7 +468,7 @@ def _render_decision_guide(rows: List[Dict[str, Any]]) -> str: ) -def _render_resources(resources: List[Dict[str, Any]], pack_name: str) -> str: +def _render_resources(resources: List[Dict[str, Any]], blob_base: str) -> str: if not resources: return "

No resources.

" @@ -467,10 +493,7 @@ def _render_resources(resources: List[Dict[str, Any]], pack_name: str) -> str: if description: line.append(f" - {description}") if embedded: - embed_url = ( - "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/" - f"{pack_name}/{embedded.lstrip('/')}" - ) + embed_url = f"{blob_base}/{embedded.lstrip('/')}" line.append( f' [embedded doc]' @@ -534,14 +557,10 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) page_title = collection.get("name") or pack.get("plugin", {}).get("title") or pack.get("name") description = str(collection.get("description") or "").strip() subtitle = html.escape(description[:120].strip() + ("..." if len(description) > 120 else "")) - yaml_link = ( - "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/" - f"{pack['name']}/.catalog/collection.yaml" - ) - readme_link = ( - "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/" - f"{pack['name']}/README.md" - ) + blob_base = _pack_blob_base(pack) + has_catalog = bool(collection) + yaml_link = f"{blob_base}/.catalog/collection.yaml" + readme_link = f"{blob_base}/README.md" categories = collection.get("categories") or [] personas = collection.get("personas") or [] version = pack.get("plugin", {}).get("version") @@ -591,6 +610,14 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) overview_parts.append("

Security Model

") overview_parts.append(f"
{md_to_html(collection.get('security_model'))}
") + # README fallback: no catalog data but a README is available + if not overview_parts: + readme_content = pack.get("readme_content") or "" + if readme_content.strip(): + overview_parts.append(f"
{md_to_html(readme_content)}
") + else: + overview_parts.append("

No overview available.

") + # Skills tab contents = collection.get("contents") or {} skills_parts = ["

Skills

"] @@ -603,10 +630,10 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) skills = contents.get("skills") or [] if orchestration: skills_parts.append(f"

{'Orchestration Skill' if len(orchestration) == 1 else 'Orchestration Skills'}

") - skills_parts.append(_render_skills_list(orchestration, pack["name"], "Orchestration skill")) + skills_parts.append(_render_skills_list(orchestration, blob_base, "Orchestration skill")) if skills: skills_parts.append(f"

{'Basic Skills' if orchestration else 'Skills'}

") - skills_parts.append(_render_skills_list(skills, pack["name"])) + skills_parts.append(_render_skills_list(skills, blob_base)) guide = contents.get("skills_decision_guide") or [] if guide: skills_parts.append("

Skills Decision Guide

") @@ -615,7 +642,7 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) skills_parts.append("

No skills content.

") # Resources tab - resources_html = _render_resources(collection.get("resources") or [], pack["name"]) + resources_html = _render_resources(collection.get("resources") or [], blob_base) # Agents tab pack_mcp = [s for s in mcp_data if s.get("pack") == pack.get("name")] @@ -635,8 +662,10 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) legal = collection.get("legal_resources") or {} license_nav_href = str(legal.get("license_agreement_url") or "").strip() if not license_nav_href: - license_nav_href = "https://github.com/RHEcosystemAppEng/agentic-collections/blob/main/LICENSE" + license_nav_href = f"{blob_base}/LICENSE" license_nav_href_esc = html.escape(license_nav_href, quote=True) + license_id = str(pack.get("plugin", {}).get("license") or "").strip() + license_nav_label = f"{license_id} License" if license_id and license_id != "Unknown" else "License" return f""" @@ -659,11 +688,11 @@ def render_collection_page(pack: Dict[str, Any], mcp_data: List[Dict[str, Any]]) ← Back to Catalog
- catalog YAML → + {f'catalog YAML →' if has_catalog else ''}
@@ -708,7 +737,7 @@ def generate_collection_pages(pack_data: List[Dict[str, Any]], mcp_data: List[Di generated_files = set() count = 0 for pack in pack_data: - if not pack.get("collection"): + if not pack.get("collection") and not pack.get("readme_content") and not pack.get("skills"): continue collection = pack.get("collection") or {} collection_id = ( diff --git a/scripts/generate_mcp_data.py b/scripts/generate_mcp_data.py index 45c67e6..332e126 100644 --- a/scripts/generate_mcp_data.py +++ b/scripts/generate_mcp_data.py @@ -4,13 +4,10 @@ """ import json -import sys import re from pathlib import Path from typing import Dict, List, Any -from generate_pack_data import DOCS_PACK_DIRS - MCP_FILENAME = "mcps.json" MCP_DEPRECATED = ".mcp.json" @@ -80,8 +77,7 @@ def parse_mcp_file(pack_dir: str) -> List[Dict[str, Any]]: mcp_file = pack_path / MCP_FILENAME if deprecated_path.exists(): - print(f"Error: {pack_dir}/{MCP_DEPRECATED} is deprecated; rename to {MCP_FILENAME}", file=sys.stderr) - sys.exit(1) + print(f"Warning: {pack_dir}/{MCP_DEPRECATED} is deprecated and will be ignored; rename to {MCP_FILENAME}") if not mcp_file.exists(): return [] @@ -160,59 +156,49 @@ def load_custom_mcp_data() -> Dict[str, Any]: return {} -def generate_mcp_data() -> List[Dict[str, Any]]: +def _merge_custom_data(server: Dict[str, Any], custom_data: Dict[str, Any]) -> None: + """Merge docs/mcp.json metadata into a server dict in place.""" + server_name = server['name'] + custom = custom_data.get(server_name, {}) + server['repository'] = custom.get('repository', '') + server['tools'] = custom.get('tools', []) + server['title'] = custom.get('title', server_name) + server['tier'] = custom.get('tier', 'Official') + server['owner'] = custom.get('owner', 'Red Hat') + + +def generate_mcp_data(pack_data: List[Dict[str, Any]] | None = None) -> List[Dict[str, Any]]: """ - Generate MCP server data for all agentic packs. - Merges data from mcps.json files with custom data from docs/mcp.json. + Generate MCP server data from pack_data (marketplace packs with mcp_servers_raw) + merged with custom metadata from docs/mcp.json. + + Args: + pack_data: List of pack dicts from generate_pack_data(); each may carry + ``mcp_servers_raw`` parsed during the clone phase. Returns: List of MCP server dictionaries """ mcp_servers = [] - - # Load custom data (repository URLs and tool descriptions) custom_data = load_custom_mcp_data() - for pack_dir in DOCS_PACK_DIRS: - pack_path = Path(pack_dir) - - if not pack_path.exists(): - continue - - servers = parse_mcp_file(pack_dir) - - # Merge custom data for each server + for pack in (pack_data or []): + servers = list(pack.get("mcp_servers_raw") or []) for server in servers: - server_name = server['name'] - if server_name in custom_data: - # Add custom metadata from docs/mcp.json - server['repository'] = custom_data[server_name].get('repository', '') - server['tools'] = custom_data[server_name].get('tools', []) - server['title'] = custom_data[server_name].get('title', server_name) - server['tier'] = custom_data[server_name].get('tier', 'Official') - server['owner'] = custom_data[server_name].get('owner', 'Red Hat') - else: - # No custom data available - use defaults - server['repository'] = '' - server['tools'] = [] - server['title'] = server_name - server['tier'] = 'Official' - server['owner'] = 'Red Hat' - + _merge_custom_data(server, custom_data) mcp_servers.extend(servers) - if servers: - print(f"✓ Parsed {pack_dir}: {len(servers)} MCP server(s)") + print(f"✓ {pack.get('name', '?')}: {len(servers)} MCP server(s)") return mcp_servers if __name__ == '__main__': - # Test the script + from generate_pack_data import generate_pack_data print("Parsing MCP server configurations...") print() - servers = generate_mcp_data() + servers = generate_mcp_data(generate_pack_data()) print() print(f"Found {len(servers)} MCP servers total") diff --git a/scripts/generate_pack_data.py b/scripts/generate_pack_data.py index 663e8f6..793fb51 100644 --- a/scripts/generate_pack_data.py +++ b/scripts/generate_pack_data.py @@ -3,155 +3,46 @@ Parse agentic packs and extract plugin metadata, skills, and agents. """ -import json -import os import re from pathlib import Path from typing import Dict, List, Any import yaml import pack_registry - -# Union registry (marketplace ∪ plugins.json); docs site uses subset helper -PACK_DIRS = pack_registry.get_union_pack_dirs() -DOCS_PACK_DIRS = pack_registry.get_docs_pack_dirs() - +from catalog_site_bundle import bundle_catalog_for_site +from generate_mcp_data import parse_mcp_file def parse_yaml_frontmatter(file_path: Path) -> Dict[str, Any]: - """ - Extract YAML frontmatter from a markdown file. - - Args: - file_path: Path to the markdown file - - Returns: - Dictionary containing the frontmatter data - """ try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - - # Match YAML frontmatter (---\n...\n---) match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL) if not match: return {} - - frontmatter_text = match.group(1) - return yaml.safe_load(frontmatter_text) or {} - + return yaml.safe_load(match.group(1)) or {} except Exception as e: print(f"Warning: Failed to parse frontmatter from {file_path}: {e}") return {} -def load_plugin_titles() -> Dict[str, str]: - """ - Load plugin title mappings from docs/plugins.json. - - Returns: - Dictionary mapping plugin names to display titles - """ - plugins_file = Path('docs/plugins.json') - - if not plugins_file.exists(): - print("Warning: docs/plugins.json not found, using default titles") - return {} - - try: - with open(plugins_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Extract just the titles into a simple mapping - return {name: info['title'] for name, info in data.items()} - - except Exception as e: - print(f"Warning: Failed to load docs/plugins.json: {e}") - return {} - - -def parse_plugin_json(pack_dir: str, plugin_titles: Dict[str, str]) -> Dict[str, Any]: - """ - Parse optional plugin.json from a pack directory and merge with title from docs/plugins.json. - - Args: - pack_dir: Name of the pack directory - plugin_titles: Dictionary mapping plugin names to display titles - - Returns: - Dictionary with plugin metadata, or defaults if file doesn't exist - """ - plugin_path = Path(pack_dir) / '.claude-plugin' / 'plugin.json' - - # Default values if plugin.json doesn't exist - defaults = { - 'name': pack_dir, - 'version': '0.0.0', - 'description': f'{pack_dir} agentic collection', - 'author': {'name': 'Red Hat'}, - 'license': 'Apache-2.0', - 'keywords': [] - } - - if not plugin_path.exists(): - # Use title from plugins.json if available - if pack_dir in plugin_titles: - defaults['title'] = plugin_titles[pack_dir] - return defaults - - try: - with open(plugin_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Merge with defaults (in case some fields are missing) - result = {**defaults, **data} - - # Override with title from docs/plugins.json if available - if pack_dir in plugin_titles: - result['title'] = plugin_titles[pack_dir] - elif 'title' not in result: - # Fallback: use name as title if not set - result['title'] = result['name'] - - return result - - except Exception as e: - print(f"Warning: Failed to parse {plugin_path}: {e}") - # Use title from plugins.json if available even on error - if pack_dir in plugin_titles: - defaults['title'] = plugin_titles[pack_dir] - return defaults - - -def overlay_plugin_version_from_marketplace(pack_dir: str, plugin: Dict[str, Any]) -> None: - """ - Set plugin['version'] from marketplace/rh-agentic-collection.yml modules[].version - when present. Overrides defaults and .claude-plugin/plugin.json so the docs site - matches the Lola marketplace module version. - """ - mod = pack_registry.load_marketplace_module_by_path(pack_dir) - if not mod: - return - raw = mod.get("version") - if raw is None: - return - ver = str(raw).strip() - if ver: - plugin["version"] = ver - - def parse_skills(pack_dir: str) -> List[Dict[str, Any]]: """ - Parse skills from skills/*/SKILL.md files. - - Args: - pack_dir: Name of the pack directory - - Returns: - List of skill dictionaries with name, description, file_path + Parse skills from skills/*/SKILL.md (multi-skill pack) or SKILL.md at root (single-skill repo). """ skills = [] - skills_dir = Path(pack_dir) / 'skills' + root = Path(pack_dir) + # Single-skill repo: SKILL.md at the pack root + root_skill = root / 'SKILL.md' + if root_skill.is_file(): + frontmatter = parse_yaml_frontmatter(root_skill) + name = frontmatter.get('name', root.name) + description = frontmatter.get('description', '') + if isinstance(description, str): + description = ' '.join(description.split()) + return [{'name': name, 'description': description, 'file_path': 'SKILL.md'}] + + skills_dir = root / 'skills' if not skills_dir.exists(): return skills @@ -176,118 +67,6 @@ def parse_skills(pack_dir: str) -> List[Dict[str, Any]]: return sorted(skills, key=lambda s: s['name']) -def parse_agents(pack_dir: str) -> List[Dict[str, Any]]: - """ - Parse agents from agents/*.md files. - - Args: - pack_dir: Name of the pack directory - - Returns: - List of agent dictionaries with name, description, model, tools, file_path - """ - agents = [] - agents_dir = Path(pack_dir) / 'agents' - - if not agents_dir.exists(): - return agents - - # Find all .md files in agents directory - for agent_file in agents_dir.glob('*.md'): - frontmatter = parse_yaml_frontmatter(agent_file) - - # Extract metadata - name = frontmatter.get('name', agent_file.stem) - description = frontmatter.get('description', '') - model = frontmatter.get('model', 'inherit') - tools = frontmatter.get('tools', []) - - # Clean up description - if isinstance(description, str): - description = ' '.join(description.split()) - - agents.append({ - 'name': name, - 'description': description, - 'model': model, - 'tools': tools, - 'file_path': str(agent_file.relative_to(pack_dir)) - }) - - return sorted(agents, key=lambda a: a['name']) - - -def sanitize_for_json(obj: Any) -> Any: - """ - Convert objects to JSON-serializable format. - Handles date objects and other non-serializable types. - - Args: - obj: Object to sanitize - - Returns: - JSON-serializable version of the object - """ - from datetime import date, datetime - - if isinstance(obj, (date, datetime)): - return obj.isoformat() - elif isinstance(obj, dict): - return {k: sanitize_for_json(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [sanitize_for_json(item) for item in obj] - else: - return obj - - -def parse_docs(pack_dir: str) -> List[Dict[str, Any]]: - """ - Parse documentation files from docs/**/*.md files. - - Args: - pack_dir: Name of the pack directory - - Returns: - List of doc dictionaries with title, sources, category, file_path - """ - docs = [] - docs_dir = Path(pack_dir) / 'docs' - - if not docs_dir.exists(): - return docs - - # Files to exclude from documentation parsing - EXCLUDE_FILES = {'README.md', 'INDEX.md', 'SOURCES.md'} - - # Find all .md files recursively (excluding .ai-index directory) - for doc_file in docs_dir.rglob('*.md'): - # Skip excluded files and files in .ai-index directory - if doc_file.name in EXCLUDE_FILES or '.ai-index' in doc_file.parts: - continue - - frontmatter = parse_yaml_frontmatter(doc_file) - - # Extract metadata - title = frontmatter.get('title', doc_file.stem.replace('-', ' ').title()) - category = frontmatter.get('category', doc_file.parent.name) - sources = frontmatter.get('sources', []) - - # Ensure sources is a list and sanitize for JSON - if not isinstance(sources, list): - sources = [] - sources = sanitize_for_json(sources) - - docs.append({ - 'title': title, - 'category': category, - 'sources': sources, - 'file_path': str(doc_file.relative_to(pack_dir)) - }) - - # Sort by category first, then by title - return sorted(docs, key=lambda d: (d['category'], d['title'])) - - def detect_repo_license(repo_root: Path, pack_path: str = ".") -> str: """Best-effort SPDX identifier from LICENSE files in a cloned repository.""" candidates = [ @@ -315,18 +94,12 @@ def detect_repo_license(repo_root: Path, pack_path: str = ".") -> str: return "Unknown" -def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Dict[str, Any]]: - """ - Fetch federated modules and return them as standalone pack entries. - - Each federated pack gets ``source: "federated"`` so the docs site can - badge it differently from in-tree packs. - """ +def load_federated_packs() -> List[Dict[str, Any]]: + """Clone each marketplace module and return it as a standalone pack entry.""" import shutil import subprocess import tempfile - titles = plugin_titles if plugin_titles is not None else load_plugin_titles() modules = pack_registry.load_federated_modules() if not modules: return [] @@ -338,16 +111,6 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di try: for mod in modules: name = mod.get("name", "unknown") - fed_catalog_dir = f"federation/modules/{name}" - maturity = pack_registry.load_pack_maturity(fed_catalog_dir, repo_root) - if maturity != pack_registry.DOCS_MATURITY_PUBLISH: - label = maturity or "missing catalog" - print( - f" Skipping federated '{name}' for docs site: maturity {label} " - f"(only GREEN is published)" - ) - continue - repository = mod.get("repository", "") ref = mod.get("ref", "") description = mod.get("description", "") @@ -382,16 +145,34 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di license_id = detect_repo_license(clone_dest, pack_path) skills = parse_skills(str(pack_dir)) + # Read catalog from the cloned source repo (not from a local federation/ mirror) + cat_bundle, cat_warns = bundle_catalog_for_site(pack_path, clone_dest) + for w in cat_warns: + print(f" ⚠️ {w}") + + maturity = (cat_bundle.get("maturity") or "").strip().upper() if cat_bundle else "" + if maturity and maturity != pack_registry.DOCS_MATURITY_PUBLISH: + print(f" ⚠️ Skipping '{name}': maturity is {maturity!r} (not GREEN)") + continue + + # README fallback: read content so the UI can show it when catalog is absent + readme_path = pack_dir / "README.md" + readme_content = readme_path.read_text(encoding="utf-8") if readme_path.is_file() else None + + # MCP servers: parse from cloned content; fix pack field to use module name + mcp_servers = parse_mcp_file(str(pack_dir)) + for s in mcp_servers: + s["pack"] = name + pack = { "name": name, "path": repository, - "catalog_dir": fed_catalog_dir, - "source": "federated", "repository": repository, "ref": pack_registry.normalize_federation_ref(ref)[:12], + "icon": mod.get("icon", ""), "plugin": { "name": name, - "title": titles.get(name, name.replace("-", " ").title()), + "title": mod.get("title") or name.replace("-", " ").title(), "version": version, "description": description, "author": {"name": "External"}, @@ -401,10 +182,16 @@ def load_federated_packs(plugin_titles: Dict[str, str] | None = None) -> List[Di "skills": sorted(skills, key=lambda s: s["name"]), "agents": [], "docs": [], - "has_readme": (pack_dir / "README.md").exists(), + "has_readme": readme_content is not None, + "readme_content": readme_content, + "mcp_servers_raw": mcp_servers, } + if cat_bundle is not None: + pack["collection"] = cat_bundle packs.append(pack) - print(f" ✓ Federated '{name}': {len(skills)} skill(s) from {repository}") + catalog_status = "with catalog" if cat_bundle else "README only" + mcp_status = f", {len(mcp_servers)} MCP server(s)" if mcp_servers else "" + print(f" ✓ '{name}': {len(skills)} skill(s) from {repository} ({catalog_status}{mcp_status})") finally: shutil.rmtree(tmp, ignore_errors=True) @@ -420,41 +207,10 @@ def generate_pack_data() -> List[Dict[str, Any]]: """ packs = [] - # Load plugin title mappings from docs/plugins.json - plugin_titles = load_plugin_titles() - - for pack_dir in DOCS_PACK_DIRS: - pack_path = Path(pack_dir) - - if not pack_path.exists(): - print(f"Warning: Pack directory {pack_dir} does not exist, skipping") - continue - - docs = parse_docs(pack_dir) - - plugin = parse_plugin_json(pack_dir, plugin_titles) - overlay_plugin_version_from_marketplace(pack_dir, plugin) - - pack = { - 'name': pack_dir, - 'path': f'./{pack_dir}', - 'plugin': plugin, - 'skills': parse_skills(pack_dir), - 'agents': parse_agents(pack_dir), - 'docs': docs, - 'has_readme': (pack_path / 'README.md').exists() - } - - packs.append(pack) - - # Use title from plugin data for display - plugin_title = pack['plugin'].get('title', pack_dir) - print(f"✓ Parsed {plugin_title}: {len(pack['skills'])} skills, {len(pack['agents'])} agents, {len(docs)} docs") - - federated = load_federated_packs(plugin_titles) + federated = load_federated_packs() if federated: packs.extend(federated) - print(f"✓ Added {len(federated)} federated pack(s)") + print(f"✓ Added {len(federated)} marketplace pack(s)") return packs diff --git a/scripts/pack_registry.py b/scripts/pack_registry.py index 2157c8c..c0f2bf6 100644 --- a/scripts/pack_registry.py +++ b/scripts/pack_registry.py @@ -1,13 +1,12 @@ """ -Resolve the set of agentic pack directories from Lola marketplace + docs/plugins.json. +Resolve the set of agentic pack directories from the Lola marketplace file. -Policy (union): include every ``modules[].path`` from ``marketplace/rh-agentic-collection.yml`` -and every key from ``docs/plugins.json``, keeping only directory names that exist on disk. +The marketplace file (``marketplace/rh-agentic-collection.yml``) is the single +source of truth for pack discovery. """ from __future__ import annotations -import json import re from pathlib import Path from typing import Any, Dict, List, Optional, Set @@ -15,7 +14,6 @@ import yaml DEFAULT_MARKETPLACE = Path("marketplace/rh-agentic-collection.yml") -DEFAULT_PLUGINS_JSON = Path("docs/plugins.json") def _repo_root() -> Path: @@ -37,35 +35,17 @@ def load_marketplace_module_paths(marketplace_path: Optional[Path] = None) -> Li return out -def load_plugins_json_keys(plugins_path: Optional[Path] = None) -> List[str]: - path = plugins_path or (_repo_root() / DEFAULT_PLUGINS_JSON) - if not path.exists(): - return [] - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - if not isinstance(data, dict): - return [] - return sorted(data.keys()) - - def get_union_pack_dirs( repo_root: Optional[Path] = None, marketplace_path: Optional[Path] = None, - plugins_path: Optional[Path] = None, ) -> List[str]: """ - Sorted unique pack directory names that exist under repo root and appear in - marketplace and/or docs/plugins.json union. + Sorted pack directory names from the marketplace that exist on disk under repo root. + The marketplace file is the single source of truth for pack discovery. """ root = repo_root or _repo_root() - names: Set[str] = set() - names.update(load_marketplace_module_paths(marketplace_path)) - names.update(load_plugins_json_keys(plugins_path)) - existing: List[str] = [] - for name in sorted(names): - if (root / name).is_dir(): - existing.append(name) - return existing + names: Set[str] = set(load_marketplace_module_paths(marketplace_path)) + return [name for name in sorted(names) if (root / name).is_dir()] def load_marketplace_module_by_path( @@ -92,9 +72,9 @@ def load_marketplace_module_by_path( def federation_ref_error(ref: Any) -> Optional[str]: - """Return an error message when *ref* is missing or not a 40-character commit SHA.""" + """Return an error message when *ref* is set but not a 40-character commit SHA.""" if ref is None or not str(ref).strip(): - return "ref is required (40-character commit SHA; not a branch or tag name)" + return None # absent ref → defaults to main branch value = str(ref).strip() if not FEDERATION_REF_SHA_RE.fullmatch(value): return ( @@ -104,11 +84,12 @@ def federation_ref_error(ref: Any) -> Optional[str]: def normalize_federation_ref(ref: Any) -> str: - """Return a lowercase 40-character commit SHA.""" + """Return a lowercase 40-character commit SHA, or 'main' when ref is absent.""" err = federation_ref_error(ref) if err: raise ValueError(err) - return str(ref).strip().lower() + value = str(ref).strip() if ref is not None else "" + return value.lower() if value else "main" def validate_federated_module_entry(module: Dict[str, Any]) -> List[str]: @@ -139,77 +120,5 @@ def load_federated_modules( ] -def get_federation_module_dirs(repo_root: Optional[Path] = None) -> List[str]: - """Return ``federation/modules/`` paths that have a ``.catalog/collection.yaml`` on disk.""" - root = repo_root or _repo_root() - fed_root = root / "federation" / "modules" - if not fed_root.is_dir(): - return [] - return sorted( - f"federation/modules/{p.name}" - for p in fed_root.iterdir() - if p.is_dir() and (p / ".catalog" / "collection.yaml").is_file() - ) - - -def is_federation_module(pack_dir: str) -> bool: - """Return ``True`` if *pack_dir* lives under ``federation/modules/``.""" - return pack_dir.startswith("federation/modules/") - - -def load_plugin_title(pack_dir: str, repo_root: Optional[Path] = None) -> Optional[str]: - root = repo_root or _repo_root() - p = root / DEFAULT_PLUGINS_JSON - if not p.exists(): - return None - with open(p, "r", encoding="utf-8") as f: - data = json.load(f) - entry = data.get(pack_dir) - if isinstance(entry, dict): - t = entry.get("title") - if isinstance(t, str): - return t - return None - - -# Catalog `maturity` value that is included in GitHub Pages / docs/data.json generation. +# Catalog `maturity` value published to GitHub Pages / docs/data.json. DOCS_MATURITY_PUBLISH: str = "GREEN" - - -def load_pack_maturity(pack_dir: str, repo_root: Optional[Path] = None) -> Optional[str]: - """Return uppercase maturity from ``/.catalog/collection.yaml``, or None if missing/invalid.""" - root = repo_root or _repo_root() - path = root / pack_dir / ".catalog" / "collection.yaml" - if not path.is_file(): - return None - try: - with open(path, "r", encoding="utf-8") as f: - data = yaml.safe_load(f) or {} - except (OSError, yaml.YAMLError): - return None - m = data.get("maturity") - if isinstance(m, str) and m.strip(): - return m.strip().upper() - return None - - -def get_docs_pack_dirs( - repo_root: Optional[Path] = None, -) -> List[str]: - """Pack dirs included in GitHub Pages data.json: union registry packs whose catalog maturity is GREEN.""" - root = repo_root or _repo_root() - out: List[str] = [] - for p in get_union_pack_dirs(repo_root): - if load_pack_maturity(p, root) == DOCS_MATURITY_PUBLISH: - out.append(p) - return out - - -def get_docs_federation_module_dirs(repo_root: Optional[Path] = None) -> List[str]: - """Federation module dirs listed on GitHub Pages (catalog maturity GREEN only).""" - root = repo_root or _repo_root() - return [ - p - for p in get_federation_module_dirs(root) - if load_pack_maturity(p, root) == DOCS_MATURITY_PUBLISH - ] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..36afa3d --- /dev/null +++ b/uv.lock @@ -0,0 +1,82 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "agentic-collections-catalog" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "markdown", specifier = ">=3.6" }, + { name = "pyyaml", specifier = ">=6.0" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +]