Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions scripts/build/validate_jtbd.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,22 @@ def validate(self) -> bool:
for error in [e for e in self.errors if 'frontmatter' in e.lower()]:
print(f" ❌ {error}")

# 2. Extract step headers
# 2. Extract step headers and YAML blocks
print("\n📑 Checking step headers...")
step_headers = self.extract_step_headers(content)
print(f" Found {len(step_headers)} step header(s)")

print("\n📦 Extracting job steps (YAML blocks)...")
steps = self.extract_job_steps(content)
print(f" Found {len(steps)} job step(s)")

# Prose-only skill: no step headers AND no YAML blocks → valid
if len(step_headers) == 0 and len(steps) == 0:
print(" ℹ️ Prose-only skill (no steps defined)")
self.print_summary()
return len(self.errors) == 0

# Step-based skill: validate step headers
if not step_headers:
self.errors.append("No step headers found (expecting '## Step 1:', '## Step 2:', etc.)")
print(f" ❌ No step headers found")
Expand All @@ -306,12 +317,7 @@ def validate(self) -> bool:
else:
print(f" ✅ Steps are numbered sequentially (1-{len(step_headers)})")

# 3. Extract YAML steps
print("\n📦 Extracting job steps (YAML blocks)...")
steps = self.extract_job_steps(content)
print(f" Found {len(steps)} job step(s)")

# CRITICAL: At least 1 step must be defined
# Step-based skill: at least 1 YAML step required
if len(steps) == 0:
self.errors.append(
"No YAML step blocks found! "
Expand Down
31 changes: 20 additions & 11 deletions scripts/portal_generator/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import json
import re
from pathlib import Path
from typing import Dict, List
from typing import Dict, List, Tuple

from .parsers import parse_oas, parse_skill
from .utils import get_category
Expand Down Expand Up @@ -42,18 +42,21 @@ def _extract_api_refs(skill_data: Dict) -> List[str]:
return sorted(slugs)


def discover_skills(repo_root: Path) -> Dict[str, List[Dict]]:
def discover_skills(repo_root: Path) -> Tuple[Dict[str, List[Dict]], List[Dict]]:
"""Discover all skills in the top-level skills/ directory.

Returns a mapping of ``api_slug -> [skill_data, ...]`` built by parsing
each skill's ``urn:api:`` references so that every API mentioned in a
skill gets that skill in its list.
Returns a tuple of:
- ``skills_by_api``: mapping of ``api_slug -> [skill_data, ...]`` built by
parsing each skill's ``urn:api:`` references.
- ``all_skills``: flat list of every discovered skill (including prose-only
skills that reference no APIs).
"""
skills_by_api: Dict[str, List[Dict]] = {}
all_skills: List[Dict] = []
skills_dir = repo_root / 'skills'

if not skills_dir.exists():
return skills_by_api
return skills_by_api, all_skills

print("🔍 Scanning for skills...")

Expand All @@ -71,20 +74,26 @@ def discover_skills(repo_root: Path) -> Dict[str, List[Dict]]:

api_refs = _extract_api_refs(skill_data)
skill_data['api_refs'] = api_refs
all_skills.append(skill_data)
print(f" 🎯 Skill: {skill_data.get('name', skill_dir.name)} → APIs: {', '.join(api_refs) or 'none'}")

for api_slug in api_refs:
skills_by_api.setdefault(api_slug, []).append(skill_data)

return skills_by_api
return skills_by_api, all_skills


def discover_apis(repo_root: Path) -> List[Dict]:
"""Discover all APIs in the repository"""
def discover_apis(repo_root: Path) -> Tuple[List[Dict], List[Dict]]:
"""Discover all APIs in the repository.

Returns a tuple of (apis, all_discovered_skills) where
``all_discovered_skills`` is the flat list of every skill found,
including prose-only skills that reference no APIs.
"""
apis = []

# Discover skills once (top-level skills/ folder)
skills_by_api = discover_skills(repo_root)
skills_by_api, all_discovered_skills = discover_skills(repo_root)

print("🔍 Scanning for APIs...")

Expand Down Expand Up @@ -152,7 +161,7 @@ def discover_apis(repo_root: Path) -> List[Dict]:
apis.append(api_data)

print(f"\n✅ Discovered {len(apis)} APIs")
return apis
return apis, all_discovered_skills


def calculate_stats(apis: List[Dict]) -> Dict:
Expand Down
14 changes: 13 additions & 1 deletion scripts/portal_generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def generate(self, repo_root: Path):
self.repo_root = repo_root

# Discover APIs and skills
self.apis = discover_apis(repo_root)
self.apis, all_discovered_skills = discover_apis(repo_root)
self.public_apis = [a for a in self.apis if not a.get('private')]
self.stats = calculate_stats(self.apis)

Expand All @@ -147,6 +147,15 @@ def generate(self, repo_root: Path):
seen_slugs.add(skill['slug'])
self.all_skills.append(skill)

# Also collect prose-only skills (no API refs, not tied to any API)
for skill in all_discovered_skills:
if not skill.get('api_refs') and skill['slug'] not in seen_slugs:
seen_slugs.add(skill['slug'])
self.all_skills.append(skill)

# Update skill count to include prose-only skills
self.stats['skill_count'] = len(self.all_skills)

print(f"\n📊 Statistics:")
print(f" • {self.stats['api_count']} APIs")
print(f" • {self.stats['endpoint_count']} Endpoints")
Expand Down Expand Up @@ -305,6 +314,8 @@ def _generate_skill_pages(self):
first_api = api_by_slug.get(api_refs[0]) if api_refs else None
api_meta = _build_api_meta(first_api) if first_api else {'servers': [], 'securitySchemes': {}, 'security': []}

prose_only = skill.get('step_count', 0) == 0

html = template.render(
css_path='../assets/styles.css',
icons_path='../assets/icons',
Expand All @@ -317,6 +328,7 @@ def _generate_skill_pages(self):
proxy_url=self.proxy_url,
build_label=self.build_label,
base_url=self.base_url,
prose_only=prose_only,
)
output_path = self.output_dir / 'skills' / f"{skill['slug']}.html"
with open(output_path, 'w', encoding='utf-8') as f:
Expand Down
2 changes: 1 addition & 1 deletion scripts/portal_generator/parsers/skill_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import frontmatter
from markdown_it import MarkdownIt

_md = MarkdownIt()
_md = MarkdownIt().enable('table')

try:
from ruamel.yaml import YAML
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="auth-panel-center">
<h1 class="api-title"><a href="#overview" class="api-title-link">{{ api_name }}</a></h1>
<span class="badge badge-version">{{ api_version }}</span>
{% if is_skill_page %}
{% if is_skill_page and not prose_only %}
<div class="skill-mode-toggle-container">
<label class="toggle-switch-label">
<span class="toggle-label-text">Interactive Mode</span>
Expand Down
35 changes: 35 additions & 0 deletions scripts/portal_generator/templates/partials/skill_sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
Overview
</a>
</div>
{% if step_details %}
<div class="sidebar-tabs" role="tablist">
<button class="sidebar-tab active" role="tab" aria-selected="false" aria-controls="steps-panel"
data-tab="steps" onclick="switchSidebarTab('steps')">
Steps <span class="tab-count" id="steps-count" style="display: none;"></span>
</button>
</div>
{% endif %}
</div>
<nav class="sidebar-nav">
<ul class="nav-list">
{% if step_details %}
<div id="steps-panel" class="sidebar-panel active" role="tabpanel" aria-labelledby="steps-tab">
{% for step in step_details %}
<li>
Expand All @@ -29,6 +32,38 @@
</li>
{% endfor %}
</div>
{% else %}
<div class="sidebar-panel active">
{% if skill.prerequisites_html %}
<li>
<a href="#prerequisites-section" class="nav-link nav-skill-step" title="Prerequisites">
<span class="step-title-short">Prerequisites</span>
</a>
</li>
{% endif %}
{% if skill.tips_html %}
<li>
<a href="#tips-section" class="nav-link nav-skill-step" title="Tips and Best Practices">
<span class="step-title-short">Tips and Best Practices</span>
</a>
</li>
{% endif %}
{% if skill.troubleshooting_html %}
<li>
<a href="#troubleshooting-section" class="nav-link nav-skill-step" title="Troubleshooting">
<span class="step-title-short">Troubleshooting</span>
</a>
</li>
{% endif %}
{% if skill.related_jobs_list %}
<li>
<a href="#related-jobs-section" class="nav-link nav-skill-step" title="Related Jobs">
<span class="step-title-short">Related Jobs</span>
</a>
</li>
{% endif %}
</div>
{% endif %}
</ul>
</nav>
</aside>
8 changes: 7 additions & 1 deletion scripts/portal_generator/templates/skill_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{% block body_attrs %} class="detail-page skill-page"{% endblock %}

{% block head_scripts %}
{% if not prose_only %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" defer></script>
Expand All @@ -14,6 +15,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-xml-doc.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js" defer></script>
<script src="../assets/jsonpath-plus.min.js" defer></script>
{% endif %}
<script src="../assets/portal.js" defer></script>
{% endblock %}

Expand All @@ -22,7 +24,7 @@

{% block content %}
{% set api_name = skill_name %}
{% set api_version = skill.step_count ~ ' steps' %}
{% set api_version = 'Guide' if prose_only else skill.step_count ~ ' steps' %}
{% set is_skill_page = true %}
{% set skill_slug = skill.slug %}
{% include "partials/auth_panel.html" %}
Expand All @@ -34,6 +36,7 @@
</main>
</div>

{% if not prose_only %}
<!-- X-Origin Modal (single instance for entire page) -->
<div class="xorigin-modal" id="xorigin-modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="xorigin-modal-title">
<div class="xorigin-modal-overlay" onclick="closeXOriginModal()"></div>
Expand All @@ -47,13 +50,16 @@ <h3 id="xorigin-modal-title">Fetch Values</h3>
</div>
</div>
</div>
{% endif %}
{% endblock %}

{% block scripts %}
{% if not prose_only %}
<script>
window.__API_META__ = {{ api_meta|tojson_raw }};
window.__OP_LOOKUP__ = {{ op_lookup|tojson_raw }};
window.__PROXY_CONFIG__ = { url: {{ proxy_url|tojson }} };
window.__API_LINK_PREFIX__ = '../apis/';
</script>
{% endif %}
{% endblock %}
43 changes: 42 additions & 1 deletion scripts/portal_generator/templates/skills/skill_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,47 @@ <h3>Completion Checklist</h3>
</div>
{% endif %}

{% if skill.what_youve_built_html %}
<div id="built-section" class="skill-subsection">
<h3>What You've Built</h3>
<div class="skill-view-markdown">{{ skill.what_youve_built_html|safe }}</div>
</div>
{% endif %}

{% if skill.next_steps_html %}
<div id="next-steps-section" class="skill-subsection">
<h3>Next Steps</h3>
<div class="skill-view-markdown">{{ skill.next_steps_html|safe }}</div>
</div>
{% endif %}

{% if skill.tips_html %}
<div id="tips-section" class="skill-subsection">
<h3>Tips and Best Practices</h3>
<div class="skill-view-markdown">{{ skill.tips_html|safe }}</div>
</div>
{% endif %}

{% if skill.troubleshooting_html %}
<div id="troubleshooting-section" class="skill-subsection">
<h3>Troubleshooting</h3>
<div class="skill-view-markdown">{{ skill.troubleshooting_html|safe }}</div>
</div>
{% endif %}

{% if skill.related_jobs_list %}
<div id="related-jobs-section" class="skill-subsection">
<h3>Related Jobs</h3>
<div class="skill-view-markdown">
<ul>
{% for job in skill.related_jobs_list %}
<li><a href="../skills/{{ job.slug }}.html"><strong>{{ job.slug }}</strong></a>: {{ job.description }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}

{# Install Command Modal #}
<div class="install-modal" id="install-modal-{{ slug }}" style="display:none" role="dialog" aria-modal="true">
<div class="install-modal-overlay" onclick="closeInstallModal('{{ slug }}')"></div>
Expand All @@ -152,7 +193,7 @@ <h3>Install Command</h3>
<div class="install-modal-body">
<div class="install-command-row">
<span class="install-command-bracket">[</span>
<code class="install-command-code" id="install-cmd-{{ slug }}">npx skills add https://github.com/mulesoft/anypoint-dev-portal/ --skill {{ slug }}</code>
<code class="install-command-code" id="install-cmd-{{ slug }}">npx skills add https://github.com/mulesoft/mulesoft-dx/ --skill {{ slug }}</code>
<span class="install-command-bracket">]</span>
<button class="btn-install-copy" onclick="copyInstallFromModal('{{ slug }}', this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Expand Down
Loading
Loading