Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Waiting to run
Deploy to Cloudflare Pages / deploy (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

967 lines
50 KiB
Python

import os
import json
import shutil
import requests
import subprocess
from collections import defaultdict
from dotenv import load_dotenv
from pathlib import Path
# Load environment variables
load_dotenv()
def run_security_validation():
"""
Run security validation on all components and return the results.
Returns a dictionary with component paths as keys and security data as values.
"""
print("🔒 Running security validation on components...")
try:
# Change to cli-tool directory to run npm command
cli_tool_dir = Path(__file__).parent / 'cli-tool'
# Run security audit and generate JSON report
result = subprocess.run(
['npm', 'run', 'security-audit:json'],
cwd=cli_tool_dir,
capture_output=True,
text=True,
timeout=300 # 5 minute timeout
)
if result.returncode != 0:
print(f"⚠️ Security validation completed with warnings")
print(f" stdout: {result.stdout[:200]}")
print(f" stderr: {result.stderr[:200]}")
else:
print("✅ Security validation completed successfully")
# Read the generated security report
security_report_path = cli_tool_dir / 'security-report.json'
if not security_report_path.exists():
print("⚠️ Security report not found, skipping security metadata")
return {}
with open(security_report_path, 'r', encoding='utf-8') as f:
security_data = json.load(f)
# Transform security data into a lookup dictionary
# Key format: "agents/category/name" -> security metadata
security_lookup = {}
for component_result in security_data.get('components', []):
component_info = component_result.get('component', {})
component_path = component_info.get('path', '')
component_type = component_info.get('type', '')
if component_path:
# Extract category and name from path
# Path format: components/agents/development-team/frontend-developer.md
path_parts = component_path.replace('\\', '/').split('/')
# Handle both formats: "components/agents/..." and "cli-tool/components/agents/..."
if 'components' in path_parts:
components_idx = path_parts.index('components')
if len(path_parts) > components_idx + 3:
component_type = path_parts[components_idx + 1] # agents, commands, etc.
category = path_parts[components_idx + 2]
file_name = path_parts[components_idx + 3]
name = os.path.splitext(file_name)[0]
# Create lookup key
key = f"{component_type}/{category}/{name}"
# Extract security metadata
overall = component_result.get('overall', {})
validators = component_result.get('validators', {})
# Build validators object with detailed errors and warnings
validators_data = {}
for validator_name, validator_result in validators.items():
# Process errors to extract line/column information
processed_errors = []
for error in validator_result.get('errors', []):
error_data = {
'level': error.get('level', 'error'),
'code': error.get('code', ''),
'message': error.get('message', ''),
'timestamp': error.get('timestamp', '')
}
# Extract metadata with line/column info
metadata = error.get('metadata', {})
if metadata:
error_data['metadata'] = metadata
# Extract location info if available
if 'line' in metadata:
error_data['line'] = metadata['line']
if 'column' in metadata:
error_data['column'] = metadata['column']
if 'position' in metadata:
error_data['position'] = metadata['position']
if 'lineText' in metadata:
error_data['lineText'] = metadata['lineText']
# Extract examples array if present (for patterns with multiple matches)
if 'examples' in metadata:
error_data['examples'] = metadata['examples']
processed_errors.append(error_data)
# Process warnings similarly
processed_warnings = []
for warning in validator_result.get('warnings', []):
warning_data = {
'level': warning.get('level', 'warning'),
'code': warning.get('code', ''),
'message': warning.get('message', ''),
'timestamp': warning.get('timestamp', '')
}
# Extract metadata with line/column info
metadata = warning.get('metadata', {})
if metadata:
warning_data['metadata'] = metadata
# Extract location info if available
if 'line' in metadata:
warning_data['line'] = metadata['line']
if 'column' in metadata:
warning_data['column'] = metadata['column']
if 'position' in metadata:
warning_data['position'] = metadata['position']
if 'lineText' in metadata:
warning_data['lineText'] = metadata['lineText']
# Extract examples array if present
if 'examples' in metadata:
warning_data['examples'] = metadata['examples']
processed_warnings.append(warning_data)
validators_data[validator_name] = {
'valid': validator_result.get('valid', False),
'score': validator_result.get('score', 0),
'errorCount': validator_result.get('errorCount', 0),
'warningCount': validator_result.get('warningCount', 0),
'errors': processed_errors,
'warnings': processed_warnings,
'info': validator_result.get('info', [])
}
security_lookup[key] = {
'validated': True,
'valid': overall.get('valid', False),
'score': overall.get('score', 0),
'errorCount': overall.get('errorCount', 0),
'warningCount': overall.get('warningCount', 0),
'lastValidated': security_data.get('timestamp', ''),
'validators': validators_data
}
# Add hash if available from integrity validator
integrity_data = validators.get('integrity', {})
if 'info' in integrity_data:
for info_item in integrity_data['info']:
# info_item is a dictionary with code, message, metadata
if isinstance(info_item, dict):
metadata = info_item.get('metadata', {})
if 'fullHash' in metadata:
security_lookup[key]['hash'] = metadata['fullHash']
break
elif 'hash' in metadata:
security_lookup[key]['hash'] = metadata['hash']
break
print(f"✅ Security metadata extracted for {len(security_lookup)} components")
return security_lookup
except subprocess.TimeoutExpired:
print("⚠️ Security validation timed out after 5 minutes")
return {}
except FileNotFoundError:
print("⚠️ npm command not found, skipping security validation")
return {}
except Exception as e:
print(f"⚠️ Error running security validation: {e}")
return {}
def fetch_download_stats():
"""
Fetch download statistics from Supabase
Returns a dictionary with component_type-component_name as key and download count as value
Supports: agents, commands, mcps, settings, hooks, sandbox, skills, templates, plugins
"""
print("📊 Fetching download statistics from Supabase...")
# Define type mapping once (DRY principle)
TYPE_MAPPING = {
'agent': 'agents',
'command': 'commands',
'setting': 'settings',
'hook': 'hooks',
'mcp': 'mcps',
'skill': 'skills',
'template': 'templates',
'plugin': 'plugins',
'sandbox': 'sandbox'
}
# Get Supabase credentials (same as generate_trending_data.py)
supabase_url = os.getenv("SUPABASE_URL")
supabase_api_key = os.getenv("SUPABASE_API_KEY")
if not supabase_url or not supabase_api_key:
print("⚠️ Warning: Missing Supabase credentials, skipping download stats")
return {}
try:
headers = {
'apikey': supabase_api_key,
'Authorization': f'Bearer {supabase_api_key}'
}
# First, let's query component_downloads to aggregate counts per component
# We need to handle pagination since there can be many records
api_url = f"{supabase_url}/rest/v1/component_downloads"
all_downloads = []
offset = 0
limit = 1000
# Fetch all records with pagination
max_pages = 1000 # Safety limit (1000 pages * 1000 records = 1,000,000 max)
for page in range(max_pages):
paginated_headers = headers.copy()
paginated_headers['Range'] = f'{offset}-{offset + limit - 1}'
response = requests.get(api_url, headers=paginated_headers)
if response.status_code not in [200, 206]:
print(f" Page {page+1}: Got status {response.status_code}, stopping")
break
batch = response.json()
if not batch:
break
all_downloads.extend(batch)
# Check if we have more records to fetch
content_range = response.headers.get('content-range', '')
# If we get a range like "45000-45977/*", check if we got less than limit records
if len(batch) < limit:
break # We've reached the end
# Also check for explicit total if provided
if content_range and '/' in content_range:
parts = content_range.split('/')
if parts[1] != '*':
try:
total = int(parts[1])
if offset + limit >= total:
break
except ValueError:
pass
offset += limit
# Progress indicator every 10 pages
if (page + 1) % 10 == 0:
print(f" Fetched {len(all_downloads)} records so far...")
print(f"📊 Total records fetched: {len(all_downloads)}")
# If we fetched records, use them
if len(all_downloads) > 0:
# Process component_downloads data
downloads = all_downloads
# Aggregate downloads by component
download_counts = {}
component_totals = defaultdict(int)
for download in downloads:
component_type = download.get('component_type', '')
component_name = download.get('component_name', '')
if component_type and component_name:
# Handle case where component_name already includes category
if '/' in component_name:
category = component_name.split('/')[0]
actual_name = component_name.split('/')[-1]
else:
actual_name = component_name
category = 'general'
# Create a key for aggregation matching trending data structure
key = f"{component_type}|{category}|{actual_name}"
component_totals[key] += 1
# Convert to the format we need using the TYPE_MAPPING constant
for key, count in component_totals.items():
parts = key.split('|')
if len(parts) == 3:
component_type, category, component_name = parts
mapped_type = TYPE_MAPPING.get(component_type, component_type + 's')
final_key = f"{mapped_type}/{category}/{component_name}"
download_counts[final_key] = count
print(f"✅ Fetched and aggregated {len(download_counts)} component download stats")
return download_counts
else:
# Try alternative: fetch from download_stats table if it exists
print("⚠️ No data from component_downloads, trying download_stats table...")
alt_url = f"{supabase_url}/rest/v1/download_stats"
alt_response = requests.get(alt_url, headers=headers)
if alt_response.status_code == 200:
print("📊 Using download_stats table instead...")
stats = alt_response.json()
download_counts = {}
for stat in stats:
component_type = stat.get('component_type', '')
component_name = stat.get('component_name', '')
total_downloads = stat.get('total_downloads', 0)
# Handle case where component_name already includes category
if '/' in component_name:
category = component_name.split('/')[0]
actual_name = component_name.split('/')[-1]
else:
actual_name = component_name
category = 'general'
# Map to plural form using TYPE_MAPPING constant
mapped_type = TYPE_MAPPING.get(component_type, component_type + 's')
key = f"{mapped_type}/{category}/{actual_name}"
download_counts[key] = total_downloads
print(f"✅ Fetched stats for {len(download_counts)} components from download_stats")
return download_counts
else:
print("⚠️ No download stats available")
return {}
except Exception as e:
print(f"⚠️ Error fetching download stats: {e}")
return {}
def scan_directory_recursively(directory_path, relative_to_path=None):
"""
Recursively scan a directory and return all files with their relative paths.
"""
files_list = []
if not os.path.isdir(directory_path):
return files_list
for root, dirs, files in os.walk(directory_path):
for file in files:
file_path = os.path.join(root, file)
if relative_to_path:
relative_path = os.path.relpath(file_path, relative_to_path)
files_list.append(relative_path.replace("\\", "/"))
else:
files_list.append(file)
return files_list
def generate_components_json():
"""
Scans the cli-tool/components and cli-tool/templates directories and generates a components.json file
for the static website, including the content of each file and download statistics.
"""
components_base_path = 'cli-tool/components'
templates_base_path = 'cli-tool/templates'
plugins_path = '.claude-plugin/marketplace.json'
output_path = 'docs/components.json'
components_data = {'agents': [], 'commands': [], 'mcps': [], 'settings': [], 'hooks': [], 'sandbox': [], 'skills': [], 'loops': [], 'templates': [], 'plugins': []}
# Run security validation
security_metadata = run_security_validation()
# Fetch download statistics
download_stats = fetch_download_stats()
component_types = ['agents', 'commands', 'mcps', 'settings', 'hooks', 'sandbox', 'skills', 'loops']
print(f"Starting scan of {components_base_path} and {templates_base_path}...")
# Scan components (existing logic)
for component_type in component_types:
type_path = os.path.join(components_base_path, component_type)
if not os.path.isdir(type_path):
print(f"Warning: Directory not found for type: {component_type}")
continue
print(f"Scanning for {component_type} in {type_path}...")
# Special handling for skills - they use SKILL.md inside category/skill directories
if component_type == 'skills':
for category in os.listdir(type_path):
category_path = os.path.join(type_path, category)
if os.path.isdir(category_path) and not category.endswith('.md'):
for skill_dir in os.listdir(category_path):
skill_dir_path = os.path.join(category_path, skill_dir)
if os.path.isdir(skill_dir_path):
# Look for SKILL.md inside the skill directory
skill_file_path = os.path.join(skill_dir_path, 'SKILL.md')
if os.path.isfile(skill_file_path):
name = skill_dir # Use directory name as skill name
# Read file content
content = ''
description = ''
try:
with open(skill_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract metadata from frontmatter if available
author = ''
repo = ''
version = ''
license_field = ''
keywords = []
if content.startswith('---'):
frontmatter_end = content.find('---', 3)
if frontmatter_end != -1:
frontmatter = content[3:frontmatter_end]
for line in frontmatter.split('\n'):
if line.startswith('description:'):
description = line.split('description:', 1)[1].strip()
elif line.startswith('author:'):
author = line.split('author:', 1)[1].strip()
elif line.startswith('repo:'):
repo = line.split('repo:', 1)[1].strip()
elif line.startswith('version:'):
version = line.split('version:', 1)[1].strip()
elif line.startswith('license:'):
license_field = line.split('license:', 1)[1].strip()
elif line.startswith('tags:'):
tags_str = line.split('tags:', 1)[1].strip()
# Parse tags array [tag1, tag2, tag3]
if tags_str.startswith('[') and tags_str.endswith(']'):
tags_str = tags_str[1:-1]
keywords = [tag.strip() for tag in tags_str.split(',')]
except Exception as e:
print(f"Warning: Could not read file {skill_file_path}: {e}")
# Look up download count for this skill
download_key = f"{component_type}/{category}/{name}"
downloads = download_stats.get(download_key, 0)
# Look up security metadata for this skill
security_key = f"{component_type}/{category}/{name}"
security = security_metadata.get(security_key, {
'validated': False,
'valid': None,
'score': None,
'errorCount': 0,
'warningCount': 0,
'lastValidated': None
})
# Collect reference files (all files except SKILL.md)
references = []
for ref_file in scan_directory_recursively(skill_dir_path, skill_dir_path):
if ref_file != 'SKILL.md':
references.append(ref_file)
references.sort()
# Path includes category for proper organization
component = {
'name': name, # Just the skill directory name
'path': f"{category}/{name}", # category/skill-name format
'category': category,
'type': 'skill',
'content': content,
'description': description,
'author': author,
'repo': repo,
'version': version,
'license': license_field,
'keywords': keywords,
'downloads': downloads,
'security': security,
'references': references
}
components_data[component_type].append(component)
print(f" Processed skill: {category}/{name}")
continue # Skip the normal file scanning for skills
# Normal scanning for other component types
for category in os.listdir(type_path):
category_path = os.path.join(type_path, category)
if os.path.isdir(category_path):
for file_name in os.listdir(category_path):
file_path = os.path.join(category_path, file_name)
if os.path.isfile(file_path) and (file_name.endswith('.md') or file_name.endswith('.json')) and not file_name.endswith('.py'):
name = os.path.splitext(file_name)[0]
# Read file content
content = ''
description = ''
author = ''
repo = ''
version = ''
license_field = ''
keywords = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract description, author and repo from JSON files
if file_name.endswith('.json'):
try:
import json
json_data = json.loads(content)
if component_type == 'mcps':
# Extract description from the first mcpServer entry
if 'mcpServers' in json_data:
for server_name, server_config in json_data['mcpServers'].items():
if isinstance(server_config, dict) and 'description' in server_config:
description = server_config['description']
break # Use the first description found
elif component_type in ['settings', 'hooks']:
# Extract metadata from settings/hooks JSON files
if 'description' in json_data:
description = json_data['description']
if 'author' in json_data:
author = json_data['author']
if 'repo' in json_data:
repo = json_data['repo']
if 'version' in json_data:
version = json_data['version']
if 'license' in json_data:
license_field = json_data['license']
if 'keywords' in json_data and isinstance(json_data['keywords'], list):
keywords = json_data['keywords']
except json.JSONDecodeError:
print(f"Warning: Invalid JSON in {file_path}")
# Extract metadata from markdown frontmatter
elif file_name.endswith('.md'):
if content.startswith('---'):
frontmatter_end = content.find('---', 3)
if frontmatter_end != -1:
frontmatter = content[3:frontmatter_end]
for line in frontmatter.split('\n'):
if line.startswith('description:'):
description = line.split('description:', 1)[1].strip()
elif line.startswith('author:'):
author = line.split('author:', 1)[1].strip()
elif line.startswith('repo:'):
repo = line.split('repo:', 1)[1].strip()
elif line.startswith('version:'):
version = line.split('version:', 1)[1].strip()
elif line.startswith('license:'):
license_field = line.split('license:', 1)[1].strip()
elif line.startswith('tags:'):
tags_str = line.split('tags:', 1)[1].strip()
# Parse tags array [tag1, tag2, tag3]
if tags_str.startswith('[') and tags_str.endswith(']'):
tags_str = tags_str[1:-1]
keywords = [tag.strip() for tag in tags_str.split(',')]
except Exception as e:
print(f"Warning: Could not read file {file_path}: {e}")
# Look up download count for this component
# The key in download_stats uses the plural form: agents/category/name
download_key = f"{component_type}/{category}/{name}"
downloads = download_stats.get(download_key, 0)
# Look up security metadata for this component
security_key = f"{component_type}/{category}/{name}"
security = security_metadata.get(security_key, {
'validated': False,
'valid': None,
'score': None,
'errorCount': 0,
'warningCount': 0,
'lastValidated': None
})
component = {
'name': name,
'path': os.path.join(category, file_name).replace("\\", "/"),
'category': category,
'type': component_type[:-1], # singular form
'content': content, # Add file content
'description': description, # Add description for MCPs
'author': author, # Add author field
'repo': repo, # Add repository field
'version': version, # Add version field
'license': license_field, # Add license field
'keywords': keywords, # Add keywords field
'downloads': downloads, # Add download count
'security': security # Add security metadata
}
components_data[component_type].append(component)
# Scan templates (new logic)
if os.path.isdir(templates_base_path):
print(f"Scanning for templates in {templates_base_path}...")
for language_dir in os.listdir(templates_base_path):
language_path = os.path.join(templates_base_path, language_dir)
if os.path.isdir(language_path):
print(f" Processing language: {language_dir}")
# Collect files in the language directory (excluding examples folder)
language_files = []
for item in os.listdir(language_path):
item_path = os.path.join(language_path, item)
if os.path.isfile(item_path):
language_files.append(item)
elif os.path.isdir(item_path) and item != 'examples':
# For directories like .claude, scan recursively
if item == '.claude':
recursive_files = scan_directory_recursively(item_path, language_path)
language_files.extend(recursive_files)
else:
# For other directories, just add the directory name (optional)
pass
# Look up download count for this template
template_download_key = f"templates/{language_dir}"
template_downloads = download_stats.get(template_download_key, 0)
# Create language template entry
language_template = {
'name': language_dir,
'id': language_dir,
'type': 'template',
'subtype': 'language',
'category': 'languages',
'description': f'{language_dir.title()} project template',
'files': language_files,
'installCommand': f'npx claude-code-templates@latest --template={language_dir} --yes',
'downloads': template_downloads
}
components_data['templates'].append(language_template)
# Check for examples folder (contains frameworks)
examples_path = os.path.join(language_path, 'examples')
if os.path.isdir(examples_path):
for framework_dir in os.listdir(examples_path):
framework_path = os.path.join(examples_path, framework_dir)
if os.path.isdir(framework_path):
print(f" Processing framework: {framework_dir}")
# Collect files in the framework directory
framework_files = []
for item in os.listdir(framework_path):
item_path = os.path.join(framework_path, item)
if os.path.isfile(item_path):
framework_files.append(item)
elif os.path.isdir(item_path):
# For directories like .claude, scan recursively
if item == '.claude':
recursive_files = scan_directory_recursively(item_path, framework_path)
framework_files.extend(recursive_files)
else:
# For other directories, just add the directory name (optional)
pass
# Look up download count for this framework
framework_download_key = f"templates/{framework_dir}"
framework_downloads = download_stats.get(framework_download_key, 0)
# Create framework template entry
framework_template = {
'name': framework_dir,
'id': framework_dir,
'type': 'template',
'subtype': 'framework',
'category': 'frameworks',
'language': language_dir,
'description': f'{framework_dir.title()} with {language_dir.title()}',
'files': framework_files,
'installCommand': f'npx claude-code-templates@latest --template={framework_dir} --yes',
'downloads': framework_downloads
}
components_data['templates'].append(framework_template)
else:
print(f"Warning: Templates directory not found: {templates_base_path}")
# Load components metadata from marketplace.json (Claude Code standard)
components_marketplace_path = 'cli-tool/components/.claude-plugin/marketplace.json'
components_marketplace = None
if os.path.isfile(components_marketplace_path):
print(f"Loading components metadata from {components_marketplace_path}...")
try:
with open(components_marketplace_path, 'r', encoding='utf-8') as f:
components_marketplace = json.load(f)
print(f"✅ Loaded metadata for {len(components_marketplace.get('agents', []))} agents")
except Exception as e:
print(f"⚠️ Error reading components metadata from {components_marketplace_path}: {e}")
# Scan plugins from marketplace.json and store marketplace data
marketplace_full_data = None
if os.path.isfile(plugins_path):
print(f"Scanning for plugins in {plugins_path}...")
try:
with open(plugins_path, 'r', encoding='utf-8') as f:
marketplace_data = json.load(f)
# Store full marketplace data for inclusion in components.json
marketplace_full_data = marketplace_data
if 'plugins' in marketplace_data:
for plugin in marketplace_data['plugins']:
plugin_name = plugin.get('name', '')
plugin_description = plugin.get('description', '')
plugin_version = plugin.get('version', '1.0.0')
plugin_keywords = plugin.get('keywords', [])
plugin_author = plugin.get('author', {}).get('name', '')
# Get component lists with file names
commands_list = plugin.get('commands', [])
agents_list = plugin.get('agents', [])
mcps_list = plugin.get('mcpServers', [])
# Extract component names with category/folder from file paths
def extract_component_with_category(path):
# Extract category/name from path like "./cli-tool/components/commands/git/feature.md"
# Result should be "git/feature"
parts = path.split('/')
if len(parts) >= 2:
filename = parts[-1].replace('.md', '').replace('.json', '')
category = parts[-2]
return f"{category}/{filename}"
else:
# Fallback to just filename if path structure is unexpected
filename = path.split('/')[-1]
return filename.replace('.md', '').replace('.json', '')
commands_names = [extract_component_with_category(cmd) for cmd in commands_list]
agents_names = [extract_component_with_category(agent) for agent in agents_list]
mcps_names = [extract_component_with_category(mcp) for mcp in mcps_list]
# Look up download count for this plugin
plugin_download_key = f"plugins/{plugin_name}"
plugin_downloads = download_stats.get(plugin_download_key, 0)
# Create plugin entry
plugin_entry = {
'name': plugin_name,
'id': plugin_name,
'type': 'plugin',
'description': plugin_description,
'version': plugin_version,
'keywords': plugin_keywords,
'author': plugin_author,
'commands': len(commands_list),
'agents': len(agents_list),
'mcpServers': len(mcps_list),
'commandsList': commands_names,
'agentsList': agents_names,
'mcpServersList': mcps_names,
'installCommand': f'/plugin install {plugin_name}@claude-code-templates',
'downloads': plugin_downloads
}
components_data['plugins'].append(plugin_entry)
print(f" Processed plugin: {plugin_name}")
print(f"✅ Processed {len(components_data['plugins'])} plugins from marketplace.json")
except Exception as e:
print(f"⚠️ Error reading plugins from {plugins_path}: {e}")
else:
print(f"Warning: Plugins file not found: {plugins_path}")
# Sort components alphabetically
for component_type in components_data:
if component_type in ['templates', 'plugins']:
# Sort templates and plugins by name since they don't have path
components_data[component_type].sort(key=lambda x: x['name'])
else:
# Sort other components by path
components_data[component_type].sort(key=lambda x: x['path'])
# Add marketplace metadata (root level - our public marketplace)
if marketplace_full_data:
components_data['marketplace'] = marketplace_full_data
print("✅ Added public marketplace metadata to components.json")
# Add components marketplace metadata (Claude Code standard)
if components_marketplace:
components_data['componentsMarketplace'] = components_marketplace
print("✅ Added components marketplace metadata to components.json")
# ---------------------------------------------------------------------------
# Performance optimization: separate content from the lightweight index.
#
# docs/components.json → index without 'content' (~87% smaller)
# dashboard/public/components.json → same, served by Cloudflare Pages
# dashboard/public/component-content/{type}/{slug}.json → per-component
# raw content, loaded on demand only on detail pages.
# ---------------------------------------------------------------------------
dashboard_public_dir = 'dashboard/public'
dashboard_content_dir = os.path.join(dashboard_public_dir, 'component-content')
# Component types that carry a 'content' field
content_bearing_types = ['agents', 'commands', 'mcps', 'settings', 'hooks', 'sandbox', 'skills', 'loops']
# 1. Write per-component content files directly to dashboard/public/component-content/
os.makedirs(dashboard_content_dir, exist_ok=True)
written_content_count = 0
for ctype in content_bearing_types:
for component in components_data.get(ctype, []):
raw_content = component.get('content', '')
if not raw_content:
continue
# Slug: path with extension stripped (matches detail-page URL lookup)
slug = component['path'].replace('.md', '').replace('.json', '')
content_file_path = os.path.join(dashboard_content_dir, ctype, slug + '.json')
os.makedirs(os.path.dirname(content_file_path), exist_ok=True)
try:
with open(content_file_path, 'w', encoding='utf-8') as f:
json.dump({'content': raw_content}, f, ensure_ascii=False)
written_content_count += 1
except IOError as e:
print(f"Warning: Could not write content file {content_file_path}: {e}")
print(f"Generated {written_content_count} component content files in {dashboard_content_dir}/")
# 2. Build the legacy/docs index: strip only 'content'.
# The legacy static site (docs/js/*) renders security badges, so the
# 'security' field MUST stay in docs/components.json.
index_data = {}
for k, v in components_data.items():
if k in content_bearing_types:
index_data[k] = [{key: val for key, val in c.items() if key != 'content'} for c in v]
else:
index_data[k] = v
# 3. Write the docs index (with 'security', without 'content').
try:
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(index_data, f, indent=2, ensure_ascii=False)
print(f"Successfully generated {output_path} (index without content).")
except IOError as e:
print(f"Error writing to {output_path}: {e}")
# ---------------------------------------------------------------------------
# Dashboard-specific artifacts (Cloudflare Pages, www.aitmpl.com).
#
# The dashboard never reads the 'security' field, so we strip it here to
# shrink the payload the browser downloads. We also emit smaller,
# purpose-built artifacts so islands stop downloading the whole index:
#
# components.json → full index, no 'content', no 'security'
# counts.json → { type: count } (sidebars/plugins pages)
# components/{type}.json → per-type slice (grid loads only active type)
# search-index.json → flat [{type,name,path,description,category}]
# ---------------------------------------------------------------------------
STRIP_KEYS = {'content', 'security'}
def strip_for_dashboard(component):
return {key: val for key, val in component.items() if key not in STRIP_KEYS}
# Build the dashboard index (content + security stripped from components).
dashboard_index_data = {}
for k, v in index_data.items():
if k in content_bearing_types and isinstance(v, list):
dashboard_index_data[k] = [strip_for_dashboard(c) for c in v]
elif isinstance(v, list):
# Non-content types (e.g. mcps/settings/hooks may carry security)
dashboard_index_data[k] = [strip_for_dashboard(c) for c in v]
else:
dashboard_index_data[k] = v
try:
os.makedirs(dashboard_public_dir, exist_ok=True)
dashboard_index_path = os.path.join(dashboard_public_dir, 'components.json')
with open(dashboard_index_path, 'w', encoding='utf-8') as f:
json.dump(dashboard_index_data, f, ensure_ascii=False)
print(f"Generated {dashboard_index_path} (dashboard index, no content/security).")
except Exception as e:
print(f"Warning: Could not write dashboard index: {e}")
# counts.json — tiny per-type counts so sidebars/plugins pages stop
# downloading the full index just to read array lengths.
try:
counts = {k: len(v) for k, v in dashboard_index_data.items() if isinstance(v, list)}
counts_path = os.path.join(dashboard_public_dir, 'counts.json')
with open(counts_path, 'w', encoding='utf-8') as f:
json.dump(counts, f, ensure_ascii=False)
print(f"Generated {counts_path} ({counts}).")
except Exception as e:
print(f"Warning: Could not write counts.json: {e}")
# components/{type}.json — per-type slices so the grid loads only the
# active type instead of all 1800+ components at once.
try:
per_type_dir = os.path.join(dashboard_public_dir, 'components')
os.makedirs(per_type_dir, exist_ok=True)
for k, v in dashboard_index_data.items():
if not isinstance(v, list):
continue
type_path = os.path.join(per_type_dir, f'{k}.json')
with open(type_path, 'w', encoding='utf-8') as f:
json.dump(v, f, ensure_ascii=False)
print(f"Generated per-type index files in {per_type_dir}/")
except Exception as e:
print(f"Warning: Could not write per-type index files: {e}")
# search-index.json — flat array with only the fields the global (Cmd+K)
# search reads: type, name, path, description, category.
try:
search_index = []
for k, v in dashboard_index_data.items():
if not isinstance(v, list):
continue
for c in v:
search_index.append({
'type': c.get('type'),
'name': c.get('name'),
'path': c.get('path'),
'description': c.get('description', ''),
'category': c.get('category'),
})
search_path = os.path.join(dashboard_public_dir, 'search-index.json')
with open(search_path, 'w', encoding='utf-8') as f:
json.dump(search_index, f, ensure_ascii=False)
print(f"Generated {search_path} ({len(search_index)} entries).")
except Exception as e:
print(f"Warning: Could not write search-index.json: {e}")
# Log summary (uses original components_data which still has content for accurate counts)
print("\n--- Generation Summary ---")
for component_type, components in components_data.items():
# Skip marketplace metadata in summary (it's not a component type)
if component_type in ['marketplace', 'componentsMarketplace']:
continue
print(f" - Found and processed {len(components)} {component_type}")
if component_type == 'templates':
languages = len([t for t in components if t.get('subtype') == 'language'])
frameworks = len([t for t in components if t.get('subtype') == 'framework'])
print(f" • {languages} languages, {frameworks} frameworks")
elif component_type == 'plugins':
total_commands = sum(p.get('commands', 0) for p in components)
total_agents = sum(p.get('agents', 0) for p in components)
total_mcps = sum(p.get('mcpServers', 0) for p in components)
print(f" • {total_commands} commands, {total_agents} agents, {total_mcps} MCPs")
# Security statistics for components with security metadata
if component_type not in ['templates', 'plugins']:
validated = len([c for c in components if c.get('security', {}).get('validated', False)])
valid_components = len([c for c in components if c.get('security', {}).get('valid', False)])
avg_score = sum(c.get('security', {}).get('score', 0) for c in components if c.get('security', {}).get('validated', False)) / validated if validated > 0 else 0
print(f" • Security: {validated} validated, {valid_components} passed, avg score: {avg_score:.1f}")
print("--------------------------")
if __name__ == '__main__':
generate_components_json()