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