| |
| """ |
| 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() |
|
|