danielrosehill's picture
commit
279efce
#!/usr/bin/env python3
"""
Generate static site from slash command markdown files
"""
import os
import json
import re
from pathlib import Path
from typing import Dict, List
def parse_frontmatter(content: str) -> tuple[Dict, str]:
"""Parse YAML frontmatter from markdown content"""
frontmatter = {}
body = content
if content.startswith('---'):
parts = content.split('---', 2)
if len(parts) >= 3:
fm_text = parts[1].strip()
body = parts[2].strip()
for line in fm_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
if value.startswith('[') and value.endswith(']'):
value = [v.strip() for v in value[1:-1].split(',')]
frontmatter[key] = value
return frontmatter, body
def parse_command_file(filepath: Path) -> Dict:
"""Parse a single command markdown file"""
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
frontmatter, body = parse_frontmatter(content)
command_name = filepath.stem
category = filepath.parent.name
return {
'name': command_name,
'category': category,
'description': frontmatter.get('description', ''),
'tags': frontmatter.get('tags', []),
'content': body,
'filepath': str(filepath.relative_to('commands'))
}
def scan_commands(commands_dir: Path = Path('commands')) -> Dict[str, List[Dict]]:
"""Scan all command files and organize by category"""
categories = {}
for md_file in commands_dir.rglob('*.md'):
command = parse_command_file(md_file)
category = command['category']
if category not in categories:
categories[category] = []
categories[category].append(command)
for category in categories:
categories[category].sort(key=lambda x: x['name'])
return categories
def generate_index_html(categories: Dict[str, List[Dict]]) -> str:
"""Generate the main index.html page"""
category_cards = []
for category, commands in sorted(categories.items()):
category_display = category.replace('-', ' ').replace('_', ' ').title()
command_count = len(commands)
category_cards.append(f'''
<div class="category-card" onclick="window.location.href='category.html?cat={category}'">
<h3>{category_display}</h3>
<p class="command-count">{command_count} command{"s" if command_count != 1 else ""}</p>
</div>
''')
return f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code Linux Desktop Slash Commands</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<div class="container">
<h1>Claude Code Slash Commands</h1>
<p class="subtitle">Linux Desktop System Administration Commands</p>
</div>
</header>
<main class="container">
<div class="intro">
<p>A comprehensive collection of Claude Code slash commands for Linux desktop system administration tasks. Browse by category to find commands for AI tools, system health, hardware management, and more.</p>
</div>
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search commands..." onkeyup="filterCategories()">
</div>
<div class="categories-grid" id="categoriesGrid">
{''.join(category_cards)}
</div>
</main>
<footer>
<div class="container">
<p>Created by Daniel Rosehill | <a href="https://github.com/danielrosehill/Claude-Code-Linux-Desktop-Slash-Commands" target="_blank">GitHub Repository</a></p>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>'''
def generate_category_html() -> str:
"""Generate the category page template (uses JS to load content)"""
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Category - Claude Code Commands</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<div class="container">
<a href="index.html" class="back-link">← Back to Categories</a>
<h1 id="categoryTitle">Commands</h1>
</div>
</header>
<main class="container">
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search commands in this category..." onkeyup="filterCommands()">
</div>
<div class="commands-list" id="commandsList">
<!-- Commands will be loaded here by JavaScript -->
</div>
</main>
<footer>
<div class="container">
<p>Created by Daniel Rosehill | <a href="https://github.com/danielrosehill/Claude-Code-Linux-Desktop-Slash-Commands" target="_blank">GitHub Repository</a></p>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>'''
def generate_css() -> str:
"""Generate the CSS stylesheet"""
return '''* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #2563eb;
--secondary-color: #1e40af;
--accent-color: #3b82f6;
--bg-color: #f8fafc;
--card-bg: #ffffff;
--text-color: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--code-bg: #f1f5f9;
--success-color: #10b981;
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
header {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 40px 0;
margin-bottom: 40px;
box-shadow: var(--shadow-lg);
}
header h1 {
font-size: 2.5rem;
margin-bottom: 8px;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.back-link {
display: inline-block;
color: white;
text-decoration: none;
margin-bottom: 20px;
font-size: 1rem;
opacity: 0.9;
transition: opacity 0.2s;
}
.back-link:hover {
opacity: 1;
}
.intro {
background: var(--card-bg);
padding: 24px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: var(--shadow);
border-left: 4px solid var(--primary-color);
}
.search-box {
margin-bottom: 30px;
}
#searchInput {
width: 100%;
padding: 14px 20px;
font-size: 1rem;
border: 2px solid var(--border-color);
border-radius: 8px;
background: var(--card-bg);
transition: border-color 0.2s;
}
#searchInput:focus {
outline: none;
border-color: var(--primary-color);
}
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.category-card {
background: var(--card-bg);
padding: 30px;
border-radius: 12px;
box-shadow: var(--shadow);
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.category-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--primary-color);
}
.category-card h3 {
color: var(--primary-color);
font-size: 1.4rem;
margin-bottom: 8px;
}
.command-count {
color: var(--text-secondary);
font-size: 0.95rem;
}
.commands-list {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 40px;
}
.command-card {
background: var(--card-bg);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
border: 1px solid var(--border-color);
}
.command-header {
padding: 20px 24px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(to right, var(--card-bg), #f8fafc);
transition: background 0.2s;
}
.command-header:hover {
background: var(--code-bg);
}
.command-title {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.command-name {
font-size: 1.3rem;
color: var(--primary-color);
font-weight: 600;
font-family: 'Monaco', 'Menlo', monospace;
}
.command-description {
color: var(--text-secondary);
font-size: 0.95rem;
}
.command-actions {
display: flex;
gap: 10px;
align-items: center;
}
.copy-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
font-weight: 500;
}
.copy-btn:hover {
background: var(--secondary-color);
transform: scale(1.05);
}
.copy-btn.copied {
background: var(--success-color);
}
.expand-icon {
color: var(--text-secondary);
font-size: 1.2rem;
transition: transform 0.3s;
}
.command-header.expanded .expand-icon {
transform: rotate(180deg);
}
.command-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: var(--bg-color);
}
.command-content.expanded {
max-height: 2000px;
}
.command-body {
padding: 24px;
}
.command-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.tag {
background: var(--code-bg);
color: var(--text-secondary);
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85rem;
border: 1px solid var(--border-color);
}
.command-body pre {
background: var(--code-bg);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
border: 1px solid var(--border-color);
margin: 12px 0;
}
.command-body code {
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 0.9rem;
color: var(--text-color);
}
.command-body h2 {
color: var(--primary-color);
margin-top: 24px;
margin-bottom: 12px;
font-size: 1.3rem;
}
.command-body h3 {
color: var(--secondary-color);
margin-top: 20px;
margin-bottom: 10px;
font-size: 1.1rem;
}
.command-body ul, .command-body ol {
margin-left: 24px;
margin-bottom: 12px;
}
.command-body li {
margin-bottom: 6px;
}
.command-body p {
margin-bottom: 12px;
}
.command-body strong {
color: var(--text-color);
font-weight: 600;
}
footer {
background: var(--card-bg);
border-top: 1px solid var(--border-color);
padding: 30px 0;
margin-top: 60px;
text-align: center;
color: var(--text-secondary);
}
footer a {
color: var(--primary-color);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
header h1 {
font-size: 2rem;
}
.categories-grid {
grid-template-columns: 1fr;
}
.command-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.command-actions {
width: 100%;
justify-content: flex-end;
}
}'''
def generate_js(categories: Dict[str, List[Dict]]) -> str:
"""Generate the JavaScript file with embedded data"""
commands_json = json.dumps(categories, indent=2)
return f'''// Commands data
const commandsData = {commands_json};
// Index page functionality
function filterCategories() {{
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const cards = document.querySelectorAll('.category-card');
cards.forEach(card => {{
const categoryName = card.querySelector('h3').textContent.toLowerCase();
if (categoryName.includes(searchTerm)) {{
card.classList.remove('hidden');
}} else {{
card.classList.add('hidden');
}}
}});
}}
// Category page functionality
function loadCategoryPage() {{
const urlParams = new URLSearchParams(window.location.search);
const category = urlParams.get('cat');
if (!category || !commandsData[category]) {{
window.location.href = 'index.html';
return;
}}
const categoryTitle = document.getElementById('categoryTitle');
const commandsList = document.getElementById('commandsList');
categoryTitle.textContent = category.replace(/-/g, ' ').replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase()) + ' Commands';
const commands = commandsData[category];
commandsList.innerHTML = commands.map((cmd, index) => `
<div class="command-card" data-command-name="${{cmd.name}}">
<div class="command-header" onclick="toggleCommand(${{index}})">
<div class="command-title">
<div class="command-name">/${{cmd.name}}</div>
<div class="command-description">${{cmd.description}}</div>
</div>
<div class="command-actions">
<button class="copy-btn" onclick="copyCommand(event, '/${{cmd.name}}', this)">Copy</button>
<span class="expand-icon">▼</span>
</div>
</div>
<div class="command-content" id="content-${{index}}">
<div class="command-body">
${{cmd.tags && cmd.tags.length ? `
<div class="command-tags">
${{cmd.tags.map(tag => `<span class="tag">${{tag}}</span>`).join('')}}
</div>
` : ''}}
<div class="markdown-content">${{formatMarkdown(cmd.content)}}</div>
</div>
</div>
</div>
`).join('');
}}
function toggleCommand(index) {{
const content = document.getElementById(`content-${{index}}`);
const header = content.previousElementSibling;
content.classList.toggle('expanded');
header.classList.toggle('expanded');
}}
function copyCommand(event, commandName, button) {{
event.stopPropagation();
navigator.clipboard.writeText(commandName).then(() => {{
const originalText = button.textContent;
button.textContent = 'Copied!';
button.classList.add('copied');
setTimeout(() => {{
button.textContent = originalText;
button.classList.remove('copied');
}}, 2000);
}});
}}
function filterCommands() {{
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const cards = document.querySelectorAll('.command-card');
cards.forEach(card => {{
const commandName = card.querySelector('.command-name').textContent.toLowerCase();
const commandDesc = card.querySelector('.command-description').textContent.toLowerCase();
if (commandName.includes(searchTerm) || commandDesc.includes(searchTerm)) {{
card.classList.remove('hidden');
}} else {{
card.classList.add('hidden');
}}
}});
}}
function formatMarkdown(text) {{
// Simple markdown formatting
let html = text;
// Code blocks
html = html.replace(/```([\\s\\S]*?)```/g, '<pre><code>$1</code></pre>');
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
// Headers
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
// Bullet lists
html = html.replace(/^\\s*[-*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\\/li>)/s, '<ul>$1</ul>');
// Paragraphs
html = html.replace(/^(?!<[hup]|<li)(.+)$/gm, '<p>$1</p>');
return html;
}}
// Initialize the appropriate page
if (window.location.pathname.includes('category.html')) {{
document.addEventListener('DOMContentLoaded', loadCategoryPage);
}}'''
def main():
"""Main execution"""
print("Scanning command files...")
categories = scan_commands()
print(f"Found {len(categories)} categories with {sum(len(cmds) for cmds in categories.values())} total commands")
print("Generating index.html...")
with open('index.html', 'w', encoding='utf-8') as f:
f.write(generate_index_html(categories))
print("Generating category.html...")
with open('category.html', 'w', encoding='utf-8') as f:
f.write(generate_category_html())
print("Generating styles.css...")
with open('styles.css', 'w', encoding='utf-8') as f:
f.write(generate_css())
print("Generating script.js...")
with open('script.js', 'w', encoding='utf-8') as f:
f.write(generate_js(categories))
print("✓ Static site generated successfully!")
print("\nGenerated files:")
print(" - index.html (main page)")
print(" - category.html (category view)")
print(" - styles.css (stylesheet)")
print(" - script.js (JavaScript with data)")
print(f"\nCategories: {', '.join(sorted(categories.keys()))}")
if __name__ == '__main__':
main()