Rename project from SmartTools to CmdForge

Major rename operation:
- Rename Python package: smarttools -> cmdforge
- Update CLI entry point: smarttools -> cmdforge
- Update all imports and module references
- Update pyproject.toml with new name, URLs, entry point
- Update all HTML templates with new branding
- Update documentation (CLAUDE.md, README.md, docs/*, wiki/*)
- Update environment variables:
  - SMARTTOOLS_ENV -> CMDFORGE_ENV
  - SMARTTOOLS_REGISTRY_DB -> CMDFORGE_REGISTRY_DB
  - SMARTTOOLS_TOKEN -> CMDFORGE_TOKEN
  - SMARTTOOLS_REGISTRY_* -> CMDFORGE_REGISTRY_*
- Update Dockerfile and docker-compose.yml
- Update tests to use new package name
- Update scripts and examples
- Update package.json and tailwind.config.js

All 158 unit tests pass. The CLI is working correctly with the
new cmdforge command.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-02 22:15:55 -04:00
co-authored by Claude Opus 4.5
parent 2939dd806b
commit 497fe87fc5
113 changed files with 822 additions and 822 deletions
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CmdForge{% endblock %} - Build Custom AI Commands</title>
<!-- Meta tags -->
<meta name="description" content="{% block meta_description %}Create Unix-style pipeable tools that work with any AI provider. Provider-agnostic, composable, and community-driven.{% endblock %}">
{% block meta_extra %}{% endblock %}
<!-- Open Graph -->
<meta property="og:title" content="{% block og_title %}CmdForge{% endblock %}">
<meta property="og:description" content="{% block og_description %}Build custom AI commands in YAML{% endblock %}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ request.url }}">
{% block og_extra %}{% endblock %}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{% block twitter_title %}CmdForge{% endblock %}">
<meta name="twitter:description" content="{% block twitter_description %}Build custom AI commands in YAML{% endblock %}">
{% block twitter_extra %}{% endblock %}
<!-- Canonical URL -->
<link rel="canonical" href="{% block canonical %}{{ request.url }}{% endblock %}">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Styles -->
<link rel="stylesheet" href="{{ url_for('web.static', filename='css/main.css') }}">
{% block styles %}{% endblock %}
<!-- Schema.org -->
{% block schema %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "CmdForge",
"url": "{{ request.host_url }}",
"description": "Build custom AI commands in YAML"
}
</script>
{% endblock %}
</head>
<body class="min-h-screen bg-gray-50 text-gray-900 font-sans antialiased" data-analytics-consent="{{ 'true' if session.get('consent_analytics') else 'false' }}">
<!-- Skip to content link for accessibility -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-indigo-600 text-white px-4 py-2 rounded-md z-50">
Skip to content
</a>
<!-- Header -->
{% include "components/header.html" %}
<!-- Main content -->
<main id="main-content" class="{% block main_class %}{% endblock %}">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
{% include "components/footer.html" %}
<!-- Cookie consent banner (if not consented) -->
{% if not session.get('consent_given') %}
{% include "components/consent_banner.html" %}
{% endif %}
<!-- Scripts -->
<script src="{{ url_for('web.static', filename='js/main.js') }}" defer></script>
{% block scripts %}{% endblock %}
</body>
</html>
@@ -0,0 +1,61 @@
{# Callout/alert macros for documentation and content pages #}
{# Info callout #}
{% macro info(title='Note', content='') %}
<div class="my-4 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-r-lg">
<div class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="ml-3">
<h4 class="text-sm font-medium text-blue-800">{{ title }}</h4>
<p class="mt-1 text-sm text-blue-700">{{ content }}</p>
</div>
</div>
</div>
{% endmacro %}
{# Warning callout #}
{% macro warning(title='Warning', content='') %}
<div class="my-4 p-4 bg-amber-50 border-l-4 border-amber-500 rounded-r-lg">
<div class="flex items-start">
<svg class="w-5 h-5 text-amber-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div class="ml-3">
<h4 class="text-sm font-medium text-amber-800">{{ title }}</h4>
<p class="mt-1 text-sm text-amber-700">{{ content }}</p>
</div>
</div>
</div>
{% endmacro %}
{# Error callout #}
{% macro error(title='Important', content='') %}
<div class="my-4 p-4 bg-red-50 border-l-4 border-red-500 rounded-r-lg">
<div class="flex items-start">
<svg class="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="ml-3">
<h4 class="text-sm font-medium text-red-800">{{ title }}</h4>
<p class="mt-1 text-sm text-red-700">{{ content }}</p>
</div>
</div>
</div>
{% endmacro %}
{# Tip callout #}
{% macro tip(title='Tip', content='') %}
<div class="my-4 p-4 bg-green-50 border-l-4 border-green-500 rounded-r-lg">
<div class="flex items-start">
<svg class="w-5 h-5 text-green-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
<div class="ml-3">
<h4 class="text-sm font-medium text-green-800">{{ title }}</h4>
<p class="mt-1 text-sm text-green-700">{{ content }}</p>
</div>
</div>
</div>
{% endmacro %}
@@ -0,0 +1,47 @@
{# Code block macro with syntax highlighting and copy button #}
{% macro code(content, language='', filename='', show_line_numbers=false) %}
<div class="my-4 relative group">
{% if filename %}
<div class="bg-gray-800 text-gray-400 text-xs px-4 py-2 rounded-t-lg border-b border-gray-700">
{{ filename }}
</div>
{% endif %}
<div class="relative">
<pre class="{% if filename %}rounded-t-none{% endif %} bg-gray-900 text-gray-100 text-sm p-4 rounded-lg overflow-x-auto"><code class="language-{{ language }}">{{ content }}</code></pre>
<button type="button"
onclick="copyCode(this)"
data-code="{{ content|e }}"
class="absolute top-2 right-2 p-2 text-gray-400 hover:text-white bg-gray-800 rounded opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Copy code">
<svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</button>
</div>
</div>
{% endmacro %}
{# Inline code #}
{% macro inline(content) %}
<code class="px-1.5 py-0.5 bg-gray-100 text-gray-800 text-sm font-mono rounded">{{ content }}</code>
{% endmacro %}
{# Install command (special styling) #}
{% macro install(command) %}
<div class="my-4 flex items-center bg-gray-100 rounded-lg p-4 group">
<span class="text-gray-400 mr-2 select-none">$</span>
<code class="flex-1 text-gray-800 font-mono text-sm">{{ command }}</code>
<button type="button"
onclick="copyToClipboard('{{ command }}')"
class="ml-4 p-2 text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Copy command">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
{% endmacro %}
@@ -0,0 +1,148 @@
{# Cookie consent banner #}
<div id="consent-banner"
class="fixed bottom-0 inset-x-0 z-50 bg-white border-t border-gray-200 shadow-lg transform transition-transform duration-300"
role="dialog"
aria-labelledby="consent-title"
aria-describedby="consent-description">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div class="flex-1">
<h3 id="consent-title" class="text-sm font-semibold text-gray-900">
We value your privacy
</h3>
<p id="consent-description" class="mt-1 text-sm text-gray-600">
We use cookies to improve your experience and for analytics.
You can choose to accept all cookies or manage your preferences.
<a href="{{ url_for('web.privacy') }}" class="text-indigo-600 hover:text-indigo-800 underline">Learn more</a>
</p>
</div>
<div class="flex items-center gap-3 flex-shrink-0">
<button type="button"
onclick="openConsentPreferences()"
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors">
Manage preferences
</button>
<button type="button"
onclick="rejectConsent()"
class="px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors">
Reject all
</button>
<button type="button"
onclick="acceptAllConsent()"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors">
Accept all
</button>
</div>
</div>
</div>
</div>
<!-- Consent preferences modal -->
<div id="consent-modal"
class="hidden fixed inset-0 z-50 overflow-y-auto"
aria-modal="true"
role="dialog"
aria-labelledby="consent-modal-title">
<div class="min-h-screen px-4 text-center">
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeConsentPreferences()"></div>
<!-- Modal -->
<div class="inline-block w-full max-w-lg my-8 text-left align-middle transition-all transform bg-white shadow-xl rounded-lg">
<div class="p-6">
<h3 id="consent-modal-title" class="text-lg font-semibold text-gray-900">
Cookie Preferences
</h3>
<p class="mt-2 text-sm text-gray-600">
Choose which cookies you want to accept. Essential cookies cannot be disabled as they are required for the site to function.
</p>
<div class="mt-6 space-y-4">
<!-- Essential cookies (always on) -->
<div class="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div>
<label for="consent-essential" class="text-sm font-medium text-gray-900">Essential cookies</label>
<p class="mt-1 text-sm text-gray-500">Required for the website to function properly.</p>
</div>
<input type="checkbox" id="consent-essential" checked disabled aria-label="Essential cookies (always enabled)" class="mt-1 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<!-- Analytics cookies -->
<div class="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div>
<label for="consent-analytics" class="text-sm font-medium text-gray-900 cursor-pointer">Analytics cookies</label>
<p class="mt-1 text-sm text-gray-500">Help us understand how visitors use our site.</p>
</div>
<input type="checkbox"
id="consent-analytics"
name="analytics"
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
</div>
<!-- Advertising cookies -->
<div class="flex items-start justify-between p-4 bg-gray-50 rounded-lg">
<div>
<label for="consent-ads" class="text-sm font-medium text-gray-900 cursor-pointer">Advertising cookies</label>
<p class="mt-1 text-sm text-gray-500">Used to show relevant ads and support the project.</p>
</div>
<input type="checkbox"
id="consent-ads"
name="ads"
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
</div>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
<button type="button"
onclick="closeConsentPreferences()"
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors">
Cancel
</button>
<button type="button"
onclick="saveConsentPreferences()"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors">
Save preferences
</button>
</div>
</div>
</div>
</div>
<script>
function acceptAllConsent() {
saveConsent(true, true);
}
function rejectConsent() {
saveConsent(false, false);
}
function openConsentPreferences() {
document.getElementById('consent-modal').classList.remove('hidden');
}
function closeConsentPreferences() {
document.getElementById('consent-modal').classList.add('hidden');
}
function saveConsentPreferences() {
const analytics = document.getElementById('consent-analytics').checked;
const ads = document.getElementById('consent-ads').checked;
saveConsent(analytics, ads);
closeConsentPreferences();
}
function saveConsent(analytics, ads) {
fetch('/api/v1/consent', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({analytics: analytics, ads: ads})
}).then(() => {
document.getElementById('consent-banner').style.transform = 'translateY(100%)';
setTimeout(() => {
document.getElementById('consent-banner').remove();
}, 300);
});
}
</script>
@@ -0,0 +1,58 @@
{# Contributor card macro #}
{% macro contributor_card(contributor=none, display_name=none, slug=none, bio=none, website=none, verified=none, featured=false) %}
{% if contributor is none %}
{% set contributor = {
"display_name": display_name,
"slug": slug,
"bio": bio,
"website": website,
"verified": verified
} %}
{% endif %}
{% if featured %}
<!-- Featured contributor (horizontal layout) -->
<article class="bg-white rounded-lg border border-gray-200 shadow-sm p-6 flex items-center space-x-6">
<!-- Avatar -->
<div class="flex-shrink-0 w-16 h-16 bg-gradient-to-br from-indigo-500 to-cyan-500 rounded-full flex items-center justify-center">
<span class="text-white text-2xl font-bold">{{ contributor.display_name[0]|upper }}</span>
</div>
<!-- Info -->
<div class="flex-1 min-w-0">
<h3 class="text-xl font-semibold text-gray-900">{{ contributor.display_name }}</h3>
<p class="text-sm text-gray-500">@{{ contributor.slug }}</p>
<p class="mt-2 text-gray-600 line-clamp-2">{{ contributor.bio or 'Active community member and tool creator.' }}</p>
</div>
<!-- Actions -->
<div class="flex-shrink-0">
<a href="{{ url_for('web.publisher_profile', slug=contributor.slug) }}"
class="inline-flex items-center px-4 py-2 border border-indigo-600 text-sm font-medium rounded-md text-indigo-600 hover:bg-indigo-50 transition-colors">
View Profile
</a>
</div>
</article>
{% else %}
<!-- Regular contributor card (vertical layout) -->
<article class="bg-white rounded-lg border border-gray-200 shadow-sm p-4 text-center">
<!-- Avatar -->
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-cyan-500 rounded-full flex items-center justify-center mx-auto">
<span class="text-white text-lg font-bold">{{ contributor.display_name[0]|upper }}</span>
</div>
<!-- Info -->
<h3 class="mt-3 font-semibold text-gray-900">{{ contributor.display_name }}</h3>
<p class="text-sm text-gray-500">@{{ contributor.slug }}</p>
{% if contributor.bio %}
<p class="mt-2 text-sm text-gray-600 line-clamp-2">{{ contributor.bio }}</p>
{% endif %}
<!-- Link -->
<a href="{{ url_for('web.publisher_profile', slug=contributor.slug) }}"
class="mt-3 inline-block text-sm font-medium text-indigo-600 hover:text-indigo-800 transition-colors">
View Profile
</a>
</article>
{% endif %}
{% endmacro %}
@@ -0,0 +1,60 @@
<footer class="bg-slate-800 text-white mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Brand -->
<div class="col-span-1 md:col-span-2">
<a href="{{ url_for('web.index') }}" class="text-xl font-bold">CmdForge</a>
<p class="mt-2 text-gray-400 text-sm max-w-md">
Build custom AI commands in YAML. Create Unix-style pipeable tools that work with any AI provider.
Provider-agnostic, composable, and community-driven.
</p>
<div class="mt-4 flex space-x-4">
<a href="https://github.com/rob/cmdforge" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-white transition-colors" aria-label="GitHub">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
</svg>
</a>
<a href="https://twitter.com/cmdforge" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-white transition-colors" aria-label="Twitter">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"/>
</svg>
</a>
</div>
</div>
<!-- Resources -->
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider">Resources</h3>
<ul class="mt-4 space-y-2">
<li><a href="{{ url_for('web.docs') }}" class="text-gray-400 hover:text-white text-sm">Documentation</a></li>
<li><a href="{{ url_for('web.tutorials') }}" class="text-gray-400 hover:text-white text-sm">Tutorials</a></li>
<li><a href="{{ url_for('web.tools_browse') }}" class="text-gray-400 hover:text-white text-sm">Registry</a></li>
<li><a href="{{ url_for('forum.index') }}" class="text-gray-400 hover:text-white text-sm">Forum</a></li>
<li><a href="{{ url_for('web.community') }}" class="text-gray-400 hover:text-white text-sm">Community</a></li>
</ul>
</div>
<!-- Legal -->
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider">Legal</h3>
<ul class="mt-4 space-y-2">
<li><a href="{{ url_for('web.about') }}" class="text-gray-400 hover:text-white text-sm">About</a></li>
<li><a href="{{ url_for('web.privacy') }}" class="text-gray-400 hover:text-white text-sm">Privacy Policy</a></li>
<li><a href="{{ url_for('web.terms') }}" class="text-gray-400 hover:text-white text-sm">Terms of Service</a></li>
<li><a href="{{ url_for('web.donate') }}" class="text-gray-400 hover:text-white text-sm">Donate</a></li>
</ul>
</div>
</div>
<div class="mt-8 pt-8 border-t border-slate-700 flex flex-col sm:flex-row justify-between items-center">
<p class="text-gray-400 text-sm">
&copy; {{ now().year }} CmdForge. Open source under MIT License.
</p>
<p class="text-gray-300 text-xs mt-2 sm:mt-0">
Made with care for the developer community.
</p>
</div>
</div>
</footer>
@@ -0,0 +1,169 @@
{# Reusable form macros #}
{# Text input field #}
{% macro text_input(name, label, type='text', placeholder='', value='', required=false, error=none, help=none) %}
<div class="mb-4">
<label for="{{ name }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
{% if required %}<span class="text-red-500">*</span>{% endif %}
</label>
<input type="{{ type }}"
name="{{ name }}"
id="{{ name }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
class="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors
{% if error %}border-red-500 bg-red-50{% else %}border-gray-300{% endif %}"
aria-describedby="{{ name }}-help {{ name }}-error">
{% if help %}
<p id="{{ name }}-help" class="mt-1 text-sm text-gray-500">{{ help }}</p>
{% endif %}
{% if error %}
<p id="{{ name }}-error" class="mt-1 text-sm text-red-600" role="alert">{{ error }}</p>
{% endif %}
</div>
{% endmacro %}
{# Textarea field #}
{% macro textarea(name, label, placeholder='', value='', required=false, rows=4, error=none, help=none) %}
<div class="mb-4">
<label for="{{ name }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
{% if required %}<span class="text-red-500">*</span>{% endif %}
</label>
<textarea name="{{ name }}"
id="{{ name }}"
rows="{{ rows }}"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
class="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors resize-y
{% if error %}border-red-500 bg-red-50{% else %}border-gray-300{% endif %}"
aria-describedby="{{ name }}-help {{ name }}-error">{{ value }}</textarea>
{% if help %}
<p id="{{ name }}-help" class="mt-1 text-sm text-gray-500">{{ help }}</p>
{% endif %}
{% if error %}
<p id="{{ name }}-error" class="mt-1 text-sm text-red-600" role="alert">{{ error }}</p>
{% endif %}
</div>
{% endmacro %}
{# Select dropdown #}
{% macro select(name, label, options, selected='', required=false, error=none, help=none) %}
<div class="mb-4">
<label for="{{ name }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
{% if required %}<span class="text-red-500">*</span>{% endif %}
</label>
<select name="{{ name }}"
id="{{ name }}"
{% if required %}required{% endif %}
class="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors bg-white
{% if error %}border-red-500 bg-red-50{% else %}border-gray-300{% endif %}"
aria-describedby="{{ name }}-help {{ name }}-error">
{% for value, label in options %}
<option value="{{ value }}" {% if value == selected %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% if help %}
<p id="{{ name }}-help" class="mt-1 text-sm text-gray-500">{{ help }}</p>
{% endif %}
{% if error %}
<p id="{{ name }}-error" class="mt-1 text-sm text-red-600" role="alert">{{ error }}</p>
{% endif %}
</div>
{% endmacro %}
{# Checkbox #}
{% macro checkbox(name, label, checked=false, help=none) %}
<div class="mb-4 flex items-start">
<input type="checkbox"
name="{{ name }}"
id="{{ name }}"
{% if checked %}checked{% endif %}
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<div class="ml-3">
<label for="{{ name }}" class="text-sm font-medium text-gray-700">{{ label }}</label>
{% if help %}
<p class="text-sm text-gray-500">{{ help }}</p>
{% endif %}
</div>
</div>
{% endmacro %}
{# Primary button #}
{% macro button_primary(text, type='submit', name=none, disabled=false, full_width=false) %}
<button type="{{ type }}"
{% if name %}name="{{ name }}"{% endif %}
{% if disabled %}disabled{% endif %}
class="{% if full_width %}w-full{% endif %} inline-flex justify-center items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{{ text }}
</button>
{% endmacro %}
{# Secondary button #}
{% macro button_secondary(text, type='button', name=none, disabled=false, full_width=false) %}
<button type="{{ type }}"
{% if name %}name="{{ name }}"{% endif %}
{% if disabled %}disabled{% endif %}
class="{% if full_width %}w-full{% endif %} inline-flex justify-center items-center px-6 py-3 border-2 border-cyan-500 text-base font-medium rounded-md text-cyan-600 bg-transparent hover:bg-cyan-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{{ text }}
</button>
{% endmacro %}
{# Danger button #}
{% macro button_danger(text, type='button', name=none, disabled=false) %}
<button type="{{ type }}"
{% if name %}name="{{ name }}"{% endif %}
{% if disabled %}disabled{% endif %}
class="inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{{ text }}
</button>
{% endmacro %}
{# Ghost/link button #}
{% macro button_ghost(text, href='#') %}
<a href="{{ href }}"
class="inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800 transition-colors">
{{ text }}
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% endmacro %}
{# Form error alert #}
{% macro form_errors(errors) %}
{% if errors %}
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg" role="alert">
<div class="flex items-start">
<svg class="w-5 h-5 text-red-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">There were errors with your submission</h3>
<ul class="mt-2 text-sm text-red-700 list-disc list-inside">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{# Success alert #}
{% macro success_alert(message) %}
{% if message %}
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg" role="alert">
<div class="flex items-center">
<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="ml-3 text-sm font-medium text-green-800">{{ message }}</p>
</div>
</div>
{% endif %}
{% endmacro %}
@@ -0,0 +1,192 @@
<header class="bg-slate-800 text-white sticky top-0 z-40">
<nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" aria-label="Main navigation">
<div class="flex items-center justify-between h-16">
<!-- Logo -->
<div class="flex-shrink-0">
<a href="{{ url_for('web.index') }}" class="text-xl font-bold hover:text-indigo-400 transition-colors">
CmdForge
</a>
</div>
<!-- Desktop navigation -->
<div class="hidden md:flex items-center space-x-1">
<a href="{{ url_for('web.docs') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/docs') %}bg-slate-700{% endif %}">
Docs
</a>
<a href="{{ url_for('web.tutorials') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/tutorials') %}bg-slate-700{% endif %}">
Tutorials
</a>
<a href="{{ url_for('web.tools_browse') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/tools') %}bg-slate-700{% endif %}">
Registry
</a>
<a href="{{ url_for('forum.index') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/forum') %}bg-slate-700{% endif %}">
Forum
</a>
<a href="{{ url_for('web.community') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path.startswith('/community') %}bg-slate-700{% endif %}">
Community
</a>
<a href="{{ url_for('web.about') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors {% if request.path == '/about' %}bg-slate-700{% endif %}">
About
</a>
</div>
<!-- Right side: Search + Auth -->
<div class="hidden md:flex items-center space-x-4">
<!-- Search button -->
<button type="button"
onclick="openSearch()"
class="p-2 rounded-md hover:bg-slate-700 transition-colors"
aria-label="Search">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</button>
<!-- Donate button -->
<a href="{{ url_for('web.donate') }}"
class="px-3 py-2 rounded-md text-sm font-medium bg-indigo-600 hover:bg-indigo-700 transition-colors">
Donate
</a>
<!-- Auth links -->
{% if session.get('user') %}
<div class="relative" x-data="{ open: false }">
<button @click="open = !open"
class="flex items-center space-x-2 px-3 py-2 rounded-md hover:bg-slate-700 transition-colors">
<span class="text-sm font-medium">{{ session.user.display_name }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div x-show="open"
@click.away="open = false"
class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
<a href="{{ url_for('web.dashboard') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Dashboard</a>
<a href="{{ url_for('web.dashboard_settings') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Settings</a>
<hr class="my-1">
<form action="{{ url_for('web.logout') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Sign out
</button>
</form>
</div>
</div>
{% else %}
<a href="{{ url_for('web.login') }}"
class="px-3 py-2 rounded-md text-sm font-medium hover:bg-slate-700 transition-colors">
Sign in
</a>
{% endif %}
</div>
<!-- Mobile menu button -->
<div class="md:hidden">
<button type="button"
onclick="toggleMobileMenu()"
class="p-2 rounded-md hover:bg-slate-700 transition-colors"
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Toggle menu">
<svg id="menu-icon-open" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
<svg id="menu-icon-close" class="w-6 h-6 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="hidden md:hidden pb-4">
<div class="space-y-1">
<a href="{{ url_for('web.docs') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/docs') %}bg-slate-700{% endif %}">
Docs
</a>
<a href="{{ url_for('web.tutorials') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/tutorials') %}bg-slate-700{% endif %}">
Tutorials
</a>
<a href="{{ url_for('web.tools_browse') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/tools') %}bg-slate-700{% endif %}">
Registry
</a>
<a href="{{ url_for('forum.index') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/forum') %}bg-slate-700{% endif %}">
Forum
</a>
<a href="{{ url_for('web.community') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path.startswith('/community') %}bg-slate-700{% endif %}">
Community
</a>
<a href="{{ url_for('web.about') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700 {% if request.path == '/about' %}bg-slate-700{% endif %}">
About
</a>
<hr class="border-slate-600 my-2">
<a href="{{ url_for('web.donate') }}"
class="block px-3 py-2 rounded-md text-base font-medium bg-indigo-600 hover:bg-indigo-700 mx-3">
Donate
</a>
{% if session.get('user') %}
<a href="{{ url_for('web.dashboard') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700">
Dashboard
</a>
<form action="{{ url_for('web.logout') }}" method="POST" class="px-3 py-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="text-base font-medium hover:text-indigo-400">
Sign out
</button>
</form>
{% else %}
<a href="{{ url_for('web.login') }}"
class="block px-3 py-2 rounded-md text-base font-medium hover:bg-slate-700">
Sign in
</a>
{% endif %}
</div>
</div>
</nav>
</header>
<!-- Search modal (hidden by default) -->
<div id="search-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true">
<div class="min-h-screen px-4 text-center">
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeSearch()"></div>
<!-- Modal content -->
<div class="inline-block w-full max-w-2xl my-8 text-left align-middle transition-all transform bg-white shadow-xl rounded-lg">
<div class="p-4">
<div class="relative">
<svg class="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<input type="text"
id="search-input"
placeholder="Search tools, docs, tutorials..."
aria-label="Search tools, documentation, and tutorials"
class="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
autocomplete="off">
</div>
<div id="search-results" class="mt-4 max-h-96 overflow-y-auto">
<!-- Search results will be inserted here -->
<p class="text-sm text-gray-500 text-center py-8">Start typing to search...</p>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 rounded-b-lg flex justify-between items-center text-sm text-gray-500">
<span>Press <kbd class="px-2 py-1 bg-gray-200 rounded">ESC</kbd> to close</span>
<button onclick="closeSearch()" class="text-gray-500 hover:text-gray-700">Close</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,70 @@
{# Tool card macro #}
{% macro tool_card(tool=none, owner=none, name=none, description=none, category=none, downloads=none, version=none) %}
{% if tool is none %}
{% set tool = {
"owner": owner,
"name": name,
"description": description,
"category": category,
"downloads": downloads,
"version": version
} %}
{% endif %}
<article class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow p-4 relative">
<!-- Category badge -->
{% if tool.category %}
<span class="absolute top-3 right-3 px-2 py-1 bg-cyan-500 text-white text-xs font-medium rounded">
{{ tool.category }}
</span>
{% endif %}
<!-- Tool icon and name -->
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
<span class="text-indigo-600 font-bold text-sm">{{ tool.name[0]|upper }}</span>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-gray-900 truncate">
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
class="hover:text-indigo-600 transition-colors">
{{ tool.name }}
</a>
</h3>
<p class="text-sm text-gray-500">by {{ tool.owner }}</p>
</div>
</div>
<!-- Description -->
<p class="mt-3 text-sm text-gray-600 line-clamp-2">
{{ tool.description or 'No description available.' }}
</p>
<!-- Meta info -->
<div class="mt-4 flex items-center justify-between text-sm text-gray-500">
<span class="flex items-center">
<svg class="w-4 h-4 mr-1 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
{{ tool.downloads|default(0) }} downloads
</span>
<span class="text-xs text-gray-600">v{{ tool.version }}</span>
</div>
<!-- Install command -->
<div class="mt-4">
<div class="flex items-center bg-gray-100 rounded px-3 py-2 group">
<code class="text-xs text-gray-700 flex-1 truncate font-mono">
cmdforge install {{ tool.owner }}/{{ tool.name }}
</code>
<button type="button"
onclick="copyToClipboard('cmdforge install {{ tool.owner }}/{{ tool.name }}')"
class="ml-2 p-1 text-gray-400 hover:text-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Copy install command">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
</div>
</article>
{% endmacro %}
@@ -0,0 +1,53 @@
{# Tutorial card macro #}
{% macro tutorial_card(tutorial=none, title=none, description=none, href=none, slug=none, thumbnail=none, step_number=none) %}
{% if tutorial is none %}
{% set tutorial = {
"slug": slug,
"title": title,
"description": description,
"thumbnail": thumbnail
} %}
{% endif %}
{% if href is none and tutorial.slug %}
{% set href = url_for('web.tutorial', slug=tutorial.slug) %}
{% endif %}
<article class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden">
<!-- Thumbnail (optional) -->
{% if tutorial.thumbnail %}
<div class="aspect-video bg-gray-100 relative">
<img src="{{ tutorial.thumbnail }}"
alt="{{ tutorial.title }}"
class="w-full h-full object-cover"
loading="lazy">
</div>
{% else %}
<div class="aspect-video bg-gradient-to-br from-indigo-500 to-cyan-500 flex items-center justify-center">
<svg class="w-12 h-12 text-white opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
</div>
{% endif %}
<div class="p-4">
{% if step_number %}
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-700 mb-2">Step {{ step_number }}</p>
{% endif %}
<h3 class="text-lg font-semibold text-gray-900">
<a href="{{ href or '#' }}"
class="hover:text-indigo-600 transition-colors">
{{ tutorial.title }}
</a>
</h3>
<p class="mt-2 text-sm text-gray-600 line-clamp-2">
{{ tutorial.description or 'Learn how to use CmdForge effectively.' }}
</p>
<a href="{{ href or '#' }}"
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800 transition-colors">
Read More
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
</div>
</article>
{% endmacro %}
@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Dashboard Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{% block dashboard_header %}
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
<p class="mt-1 text-gray-600">Welcome back, {{ user.display_name }}</p>
{% endblock %}
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar Navigation -->
<aside>
<nav class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<a href="{{ url_for('web.dashboard') }}"
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'overview' else 'text-gray-700 hover:bg-gray-50' }}">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
Overview
</a>
<a href="{{ url_for('web.dashboard_tools') }}"
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'tools' else 'text-gray-700 hover:bg-gray-50' }}">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
My Tools
</a>
<a href="{{ url_for('web.dashboard_tokens') }}"
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'tokens' else 'text-gray-700 hover:bg-gray-50' }}">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
API Tokens
</a>
<a href="{{ url_for('web.dashboard_settings') }}"
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'settings' else 'text-gray-700 hover:bg-gray-50' }}">
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Settings
</a>
</nav>
<!-- Quick Stats (mobile hidden) -->
<div class="hidden lg:block mt-6 bg-white rounded-lg border border-gray-200 p-4">
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Quick Stats</h3>
<dl class="space-y-2">
<div class="flex justify-between">
<dt class="text-sm text-gray-600">Tools</dt>
<dd class="text-sm font-medium text-gray-900">{{ stats.tools_count if stats else 0 }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-sm text-gray-600">Downloads</dt>
<dd class="text-sm font-medium text-gray-900">{{ (stats.total_downloads if stats else 0)|format_number }}</dd>
</div>
</dl>
</div>
</aside>
<!-- Main Content -->
<main class="lg:col-span-3">
{% block dashboard_content %}{% endblock %}
</main>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,135 @@
{% extends "dashboard/base.html" %}
{% set active_page = 'overview' %}
{% block title %}Dashboard - CmdForge{% endblock %}
{% block dashboard_content %}
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="flex items-center">
<div class="p-3 bg-indigo-100 rounded-lg">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Published Tools</p>
<p class="text-2xl font-bold text-gray-900">{{ stats.tools_count }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="flex items-center">
<div class="p-3 bg-green-100 rounded-lg">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">Total Downloads</p>
<p class="text-2xl font-bold text-gray-900">{{ stats.total_downloads|format_number }}</p>
</div>
</div>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="flex items-center">
<div class="p-3 bg-cyan-100 rounded-lg">
<svg class="w-6 h-6 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm text-gray-500">API Tokens</p>
<p class="text-2xl font-bold text-gray-900">{{ stats.tokens_count }}</p>
</div>
</div>
</div>
</div>
<!-- Recent Tools -->
<div class="bg-white rounded-lg border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">Your Tools</h2>
<a href="{{ url_for('web.docs', path='publishing') }}"
class="text-sm text-indigo-600 hover:text-indigo-800">
+ Publish New Tool
</a>
</div>
{% if tools %}
<ul class="divide-y divide-gray-200">
{% for tool in tools[:5] %}
<li class="px-6 py-4 flex items-center justify-between">
<div>
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
class="font-medium text-gray-900 hover:text-indigo-600">
{{ tool.name }}
</a>
<p class="text-sm text-gray-500">v{{ tool.version }}</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-900">{{ tool.downloads|format_number }} downloads</p>
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
</div>
</li>
{% endfor %}
</ul>
{% if tools|length > 5 %}
<div class="px-6 py-4 border-t border-gray-200">
<a href="{{ url_for('web.dashboard_tools') }}"
class="text-sm text-indigo-600 hover:text-indigo-800">
View all {{ tools|length }} tools
</a>
</div>
{% endif %}
{% else %}
<div class="px-6 py-12 text-center">
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<p class="mt-4 text-gray-600">You haven't published any tools yet.</p>
<a href="{{ url_for('web.docs', path='publishing') }}"
class="mt-4 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Publish Your First Tool
</a>
</div>
{% endif %}
</div>
<!-- Recent Activity -->
{% if recent_activity %}
<div class="mt-6 bg-white rounded-lg border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Recent Activity</h2>
</div>
<ul class="divide-y divide-gray-200">
{% for activity in recent_activity[:5] %}
<li class="px-6 py-4 flex items-start">
<div class="flex-shrink-0">
{% if activity.type == 'download' %}
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
</svg>
</div>
{% elif activity.type == 'publish' %}
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
</div>
{% endif %}
</div>
<div class="ml-4">
<p class="text-sm text-gray-900">{{ activity.description }}</p>
<p class="text-xs text-gray-500">{{ activity.timestamp|timeago }}</p>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,254 @@
{% extends "dashboard/base.html" %}
{% from "components/forms.html" import text_input, textarea, button_primary, form_errors, success_alert %}
{% set active_page = 'settings' %}
{% block title %}Settings - CmdForge Dashboard{% endblock %}
{% block dashboard_header %}
<h1 class="text-2xl font-bold text-gray-900">Settings</h1>
<p class="mt-1 text-gray-600">Manage your profile and account settings</p>
{% endblock %}
{% block dashboard_content %}
{{ success_alert(success_message) }}
{{ form_errors(errors) }}
<!-- Profile Settings -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Profile</h2>
<p class="text-sm text-gray-600">This information will be displayed on your public profile.</p>
</div>
<form action="{{ url_for('web.dashboard_settings') }}" method="POST" class="p-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form" value="profile">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="display_name" class="block text-sm font-medium text-gray-700 mb-1">
Display name
</label>
<input type="text"
name="display_name"
id="display_name"
value="{{ user.display_name }}"
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="slug" class="block text-sm font-medium text-gray-700 mb-1">
Username
</label>
<div class="flex">
<span class="inline-flex items-center px-3 text-gray-500 bg-gray-100 border border-r-0 border-gray-300 rounded-l-lg">
@
</span>
<input type="text"
name="slug"
id="slug"
value="{{ user.slug }}"
disabled
class="flex-1 px-4 py-2 border border-gray-300 rounded-r-lg bg-gray-50 text-gray-500">
</div>
<p class="mt-1 text-xs text-gray-500">Username cannot be changed</p>
</div>
</div>
<div class="mt-6">
<label for="bio" class="block text-sm font-medium text-gray-700 mb-1">
Bio
</label>
<textarea name="bio"
id="bio"
rows="3"
placeholder="Tell others a bit about yourself..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-y">{{ user.bio or '' }}</textarea>
<p class="mt-1 text-xs text-gray-500">Brief description for your profile. Max 500 characters.</p>
</div>
<div class="mt-6">
<label for="website" class="block text-sm font-medium text-gray-700 mb-1">
Website
</label>
<input type="url"
name="website"
id="website"
value="{{ user.website or '' }}"
placeholder="https://example.com"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="mt-6 flex justify-end">
<button type="submit"
class="px-6 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Save Profile
</button>
</div>
</form>
</div>
<!-- Email Settings -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Email</h2>
<p class="text-sm text-gray-600">Your email address for account notifications.</p>
</div>
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900">{{ user.email }}</p>
{% if user.verified %}
<p class="text-xs text-green-600 flex items-center mt-1">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
Verified
</p>
{% else %}
<p class="text-xs text-amber-600 flex items-center mt-1">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
Not verified
</p>
{% endif %}
</div>
{% if not user.verified %}
<button type="button"
onclick="resendVerification()"
class="text-sm text-indigo-600 hover:text-indigo-800">
Resend verification
</button>
{% endif %}
</div>
</div>
</div>
<!-- Password Change -->
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-semibold text-gray-900">Change Password</h2>
<p class="text-sm text-gray-600">Update your password to keep your account secure.</p>
</div>
<form action="{{ url_for('web.dashboard_settings') }}" method="POST" class="p-6">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form" value="password">
<div class="space-y-4 max-w-md">
<div>
<label for="current_password" class="block text-sm font-medium text-gray-700 mb-1">
Current password
</label>
<input type="password"
name="current_password"
id="current_password"
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="new_password" class="block text-sm font-medium text-gray-700 mb-1">
New password
</label>
<input type="password"
name="new_password"
id="new_password"
required
minlength="8"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<p class="mt-1 text-xs text-gray-500">Minimum 8 characters</p>
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-gray-700 mb-1">
Confirm new password
</label>
<input type="password"
name="confirm_password"
id="confirm_password"
required
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<div class="mt-6 flex justify-end">
<button type="submit"
class="px-6 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Update Password
</button>
</div>
</form>
</div>
<!-- Danger Zone -->
<div class="bg-white rounded-lg border border-red-200 overflow-hidden">
<div class="px-6 py-4 border-b border-red-200 bg-red-50">
<h2 class="text-lg font-semibold text-red-800">Danger Zone</h2>
<p class="text-sm text-red-600">Irreversible actions that affect your account.</p>
</div>
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900">Delete Account</p>
<p class="text-sm text-gray-500">Permanently delete your account and all associated data.</p>
</div>
<button type="button"
onclick="confirmDeleteAccount()"
class="px-4 py-2 text-sm font-medium text-red-600 border border-red-300 rounded-md hover:bg-red-50">
Delete Account
</button>
</div>
</div>
</div>
<script>
async function resendVerification() {
try {
const response = await fetch('/api/v1/me/resend-verification', {method: 'POST'});
if (response.ok) {
alert('Verification email sent! Please check your inbox.');
} else {
alert('Failed to send verification email. Please try again later.');
}
} catch (err) {
alert('Failed to send verification email. Please try again later.');
}
}
function confirmDeleteAccount() {
const confirmed = confirm(
'Are you absolutely sure you want to delete your account?\n\n' +
'This will permanently delete:\n' +
'- Your profile\n' +
'- All your published tools\n' +
'- All your API tokens\n\n' +
'This action cannot be undone.'
);
if (confirmed) {
const doubleConfirm = prompt('Type "DELETE" to confirm account deletion:');
if (doubleConfirm === 'DELETE') {
deleteAccount();
}
}
}
async function deleteAccount() {
try {
const response = await fetch('/api/v1/me', {method: 'DELETE'});
if (response.ok) {
window.location.href = '/?deleted=1';
} else {
alert('Failed to delete account. Please try again or contact support.');
}
} catch (err) {
alert('Failed to delete account. Please try again or contact support.');
}
}
</script>
{% endblock %}
@@ -0,0 +1,312 @@
{% extends "dashboard/base.html" %}
{% set active_page = 'tokens' %}
{% block title %}API Tokens - CmdForge Dashboard{% endblock %}
{% block dashboard_header %}
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">API Tokens</h1>
<p class="mt-1 text-gray-600">Manage tokens for CLI and API access</p>
</div>
<button type="button"
onclick="openCreateTokenModal()"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Create Token
</button>
</div>
{% endblock %}
{% block dashboard_content %}
<!-- Info Banner -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<div class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="ml-3">
<h4 class="text-sm font-medium text-blue-800">About API Tokens</h4>
<p class="mt-1 text-sm text-blue-700">
API tokens are used to authenticate with the CmdForge registry from the CLI.
Use <code class="px-1 bg-blue-100 rounded">cmdforge auth login</code> to authenticate,
or set the <code class="px-1 bg-blue-100 rounded">CMDFORGE_TOKEN</code> environment variable.
</p>
</div>
</div>
</div>
{% if tokens %}
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Last Used
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for token in tokens %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-10 h-10 bg-cyan-100 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-900">{{ token.name }}</p>
<p class="text-xs text-gray-500 font-mono">st_...{{ token.token_suffix }}</p>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ token.created_at|date_format }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ token.last_used_at|timeago if token.last_used_at else 'Never' }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if token.revoked_at %}
<span class="px-2 py-1 text-xs font-medium text-red-800 bg-red-100 rounded-full">
Revoked
</span>
{% else %}
<span class="px-2 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
Active
</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{% if not token.revoked_at %}
<button type="button"
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
class="text-red-600 hover:text-red-900">
Revoke
</button>
{% else %}
<span class="text-gray-400">Revoked {{ token.revoked_at|timeago }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<!-- Empty State -->
<div class="bg-white rounded-lg border border-gray-200 text-center py-16">
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No API tokens</h3>
<p class="mt-2 text-gray-600 max-w-sm mx-auto">
Create an API token to authenticate with the registry from the command line.
</p>
<button type="button"
onclick="openCreateTokenModal()"
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Create Your First Token
</button>
</div>
{% endif %}
<!-- Create Token Modal -->
<div id="create-token-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
<div class="min-h-screen px-4 text-center">
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closeCreateTokenModal()"></div>
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
<form id="create-token-form" onsubmit="createToken(event)">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900">Create API Token</h3>
<p class="mt-2 text-sm text-gray-600">
Give your token a descriptive name so you can identify it later.
</p>
<div class="mt-4">
<label for="token-name" class="block text-sm font-medium text-gray-700 mb-1">
Token name
</label>
<input type="text"
name="name"
id="token-name"
required
placeholder="e.g., Laptop CLI, CI/CD Pipeline"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
</div>
</div>
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
<button type="button"
onclick="closeCreateTokenModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Create Token
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Token Created Modal (shows the token once) -->
<div id="token-created-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
<div class="min-h-screen px-4 text-center">
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
<div class="inline-block w-full max-w-lg my-8 text-left align-middle bg-white shadow-xl rounded-lg">
<div class="p-6">
<div class="flex items-center mb-4">
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<h3 class="ml-3 text-lg font-semibold text-gray-900">Token Created</h3>
</div>
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
<div class="flex items-start">
<svg class="w-5 h-5 text-amber-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<p class="ml-3 text-sm text-amber-800">
<strong>Copy your token now.</strong> You won't be able to see it again!
</p>
</div>
</div>
<div class="bg-gray-100 rounded-lg p-4">
<div class="flex items-center justify-between">
<code id="new-token-value" class="text-sm font-mono text-gray-800 break-all"></code>
<button type="button"
onclick="copyNewToken()"
class="ml-4 p-2 text-gray-400 hover:text-gray-600 flex-shrink-0">
<svg id="copy-token-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg id="check-token-icon" class="w-5 h-5 hidden text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</button>
</div>
</div>
<p class="mt-4 text-sm text-gray-600">
To use this token, run:
</p>
<pre class="mt-2 bg-gray-900 text-gray-100 text-sm p-3 rounded-lg overflow-x-auto"><code>export CMDFORGE_TOKEN="<span id="token-in-export"></span>"</code></pre>
</div>
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end">
<button type="button"
onclick="closeTokenCreatedModal()"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Done
</button>
</div>
</div>
</div>
</div>
<script>
function openCreateTokenModal() {
document.getElementById('create-token-modal').classList.remove('hidden');
document.getElementById('token-name').focus();
}
function closeCreateTokenModal() {
document.getElementById('create-token-modal').classList.add('hidden');
document.getElementById('token-name').value = '';
}
async function createToken(event) {
event.preventDefault();
const form = event.target;
const name = form.querySelector('[name="name"]').value;
try {
const response = await fetch('/dashboard/api/tokens', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name})
});
if (response.ok) {
const result = await response.json();
closeCreateTokenModal();
showNewToken(result.data.token);
} else {
const error = await response.json();
alert(error.error || 'Failed to create token');
}
} catch (err) {
alert('Failed to create token. Please try again.');
}
}
function showNewToken(token) {
document.getElementById('new-token-value').textContent = token;
document.getElementById('token-in-export').textContent = token;
document.getElementById('token-created-modal').classList.remove('hidden');
}
function closeTokenCreatedModal() {
document.getElementById('token-created-modal').classList.add('hidden');
window.location.reload();
}
function copyNewToken() {
const token = document.getElementById('new-token-value').textContent;
navigator.clipboard.writeText(token).then(() => {
document.getElementById('copy-token-icon').classList.add('hidden');
document.getElementById('check-token-icon').classList.remove('hidden');
setTimeout(() => {
document.getElementById('copy-token-icon').classList.remove('hidden');
document.getElementById('check-token-icon').classList.add('hidden');
}, 2000);
});
}
async function revokeToken(tokenId, tokenName) {
if (!confirm(`Are you sure you want to revoke "${tokenName}"? This cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/dashboard/api/tokens/${tokenId}`, {
method: 'DELETE'
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert(error.error || 'Failed to revoke token');
}
} catch (err) {
alert('Failed to revoke token. Please try again.');
}
}
</script>
{% endblock %}
@@ -0,0 +1,259 @@
{% extends "dashboard/base.html" %}
{% set active_page = 'tools' %}
{% block title %}My Tools - CmdForge Dashboard{% endblock %}
{% block dashboard_header %}
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">My Tools</h1>
<p class="mt-1 text-gray-600">Manage your published tools</p>
</div>
<a href="{{ url_for('web.docs', path='publishing') }}"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Publish New Tool
</a>
</div>
{% endblock %}
{% block dashboard_content %}
{% if tools %}
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tool
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Version
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Downloads
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for tool in tools %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<div class="ml-4">
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
class="text-sm font-medium text-gray-900 hover:text-indigo-600">
{{ tool.name }}
</a>
<p class="text-sm text-gray-500 truncate max-w-xs">{{ tool.description or 'No description' }}</p>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-gray-900">v{{ tool.version }}</span>
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{ tool.downloads|format_number }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if tool.deprecated %}
<span class="px-2 py-1 text-xs font-medium text-amber-800 bg-amber-100 rounded-full">
Deprecated
</span>
{% else %}
<span class="px-2 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
Active
</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end gap-2">
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
class="text-indigo-600 hover:text-indigo-900">View</a>
<button type="button"
onclick="openDeprecateModal('{{ tool.owner }}', '{{ tool.name }}', {{ 'true' if tool.deprecated else 'false' }})"
class="text-gray-400 hover:text-amber-600">
{% if tool.deprecated %}Restore{% else %}Deprecate{% endif %}
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pending PRs Section -->
{% if pending_prs %}
<div class="mt-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Pending Publications</h2>
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
<ul class="divide-y divide-gray-200">
{% for pr in pending_prs %}
<li class="px-6 py-4 flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900">{{ pr.owner }}/{{ pr.name }} v{{ pr.version }}</p>
<p class="text-xs text-gray-500">Submitted {{ pr.created_at|timeago }}</p>
</div>
<div class="flex items-center gap-3">
<span class="px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full">
{{ pr.status }}
</span>
<a href="{{ pr.pr_url }}"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-indigo-600 hover:text-indigo-800">
View PR
</a>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="bg-white rounded-lg border border-gray-200 text-center py-16">
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No tools yet</h3>
<p class="mt-2 text-gray-600 max-w-sm mx-auto">
You haven't published any tools. Create your first tool and share it with the community.
</p>
<a href="{{ url_for('web.docs', path='publishing') }}"
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Learn How to Publish
</a>
</div>
{% endif %}
<!-- Deprecate Modal -->
<div id="deprecate-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
<div class="min-h-screen px-4 text-center">
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closeDeprecateModal()"></div>
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
<form id="deprecate-form" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="owner" id="deprecate-owner">
<input type="hidden" name="name" id="deprecate-name">
<div class="p-6">
<h3 id="deprecate-title" class="text-lg font-semibold text-gray-900">Deprecate Tool</h3>
<p id="deprecate-desc" class="mt-2 text-sm text-gray-600">
Mark this tool as deprecated. Users will see a warning when viewing or installing it.
</p>
<div id="deprecate-fields" class="mt-4 space-y-4">
<div>
<label for="deprecated_message" class="block text-sm font-medium text-gray-700 mb-1">
Deprecation message (optional)
</label>
<textarea name="deprecated_message"
id="deprecated_message"
rows="2"
placeholder="e.g., This tool has been superseded by..."
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"></textarea>
</div>
<div>
<label for="replacement" class="block text-sm font-medium text-gray-700 mb-1">
Replacement tool (optional)
</label>
<input type="text"
name="replacement"
id="replacement"
placeholder="owner/tool-name"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
</div>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
<button type="button"
onclick="closeDeprecateModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
Cancel
</button>
<button type="submit"
id="deprecate-submit"
class="px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-md hover:bg-amber-700">
Deprecate
</button>
</div>
</form>
</div>
</div>
</div>
<script>
let deprecateAction = '';
function openDeprecateModal(owner, name, isDeprecated) {
document.getElementById('deprecate-owner').value = owner;
document.getElementById('deprecate-name').value = name;
if (isDeprecated) {
document.getElementById('deprecate-title').textContent = 'Restore Tool';
document.getElementById('deprecate-desc').textContent = 'Remove the deprecation notice from this tool.';
document.getElementById('deprecate-fields').classList.add('hidden');
document.getElementById('deprecate-submit').textContent = 'Restore';
document.getElementById('deprecate-submit').classList.remove('bg-amber-600', 'hover:bg-amber-700');
document.getElementById('deprecate-submit').classList.add('bg-green-600', 'hover:bg-green-700');
deprecateAction = `/dashboard/api/tools/${owner}/${name}/undeprecate`;
} else {
document.getElementById('deprecate-title').textContent = 'Deprecate Tool';
document.getElementById('deprecate-desc').textContent = 'Mark this tool as deprecated. Users will see a warning.';
document.getElementById('deprecate-fields').classList.remove('hidden');
document.getElementById('deprecate-submit').textContent = 'Deprecate';
document.getElementById('deprecate-submit').classList.remove('bg-green-600', 'hover:bg-green-700');
document.getElementById('deprecate-submit').classList.add('bg-amber-600', 'hover:bg-amber-700');
deprecateAction = `/dashboard/api/tools/${owner}/${name}/deprecate`;
}
document.getElementById('deprecate-modal').classList.remove('hidden');
}
function closeDeprecateModal() {
document.getElementById('deprecate-modal').classList.add('hidden');
}
document.getElementById('deprecate-form').addEventListener('submit', async function(e) {
e.preventDefault();
const message = document.getElementById('deprecated_message')?.value || '';
const replacement = document.getElementById('replacement')?.value || '';
try {
const response = await fetch(deprecateAction, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({deprecated_message: message, replacement: replacement})
});
if (response.ok) {
window.location.reload();
} else {
const error = await response.json();
alert((error.error && error.error.message) || 'Failed to update tool status');
}
} catch (err) {
alert('Failed to update tool status. Please try again.');
}
});
</script>
{% endblock %}
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Page Not Found - CmdForge{% endblock %}
{% block content %}
<div class="min-h-[60vh] flex items-center justify-center px-4">
<div class="text-center max-w-md">
<p class="text-6xl font-bold text-indigo-600">404</p>
<h1 class="mt-4 text-3xl font-bold text-gray-900">Page not found</h1>
<p class="mt-4 text-gray-600">
Sorry, we couldn't find the page you're looking for. It may have been moved or deleted.
</p>
<div class="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="{{ url_for('web.home') }}"
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Go Home
</a>
<a href="{{ url_for('web.tools') }}"
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
Browse Tools
</a>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}Server Error - CmdForge{% endblock %}
{% block content %}
<div class="min-h-[60vh] flex items-center justify-center px-4">
<div class="text-center max-w-md">
<p class="text-6xl font-bold text-red-600">500</p>
<h1 class="mt-4 text-3xl font-bold text-gray-900">Something went wrong</h1>
<p class="mt-4 text-gray-600">
We're sorry, but something went wrong on our end. Please try again later or contact support if the problem persists.
</p>
<div class="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="{{ url_for('web.home') }}"
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Go Home
</a>
<a href="https://github.com/your-org/cmdforge/issues"
target="_blank"
rel="noopener noreferrer"
class="w-full sm:w-auto inline-flex justify-center items-center px-6 py-3 text-base font-medium text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50">
Report Issue
</a>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,109 @@
{% extends "base.html" %}
{% block title %}{{ category.name }} - Forum{% endblock %}
{% block meta_description %}{{ category.description }}{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Breadcrumb -->
<nav class="text-sm text-gray-500 mb-4">
<a href="{{ url_for('forum.index') }}" class="hover:text-indigo-600">Forum</a>
<span class="mx-2">/</span>
<span class="text-gray-900">{{ category.name }}</span>
</nav>
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">{{ category.name }}</h1>
<p class="mt-1 text-gray-600">{{ category.description }}</p>
</div>
<a href="{{ url_for('forum.new_topic', slug=category.slug) }}"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
New Topic
</a>
</div>
</div>
</div>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{% if topics %}
<!-- Topics list -->
<div class="bg-white border border-gray-200 rounded-lg divide-y divide-gray-100">
{% for topic in topics %}
<a href="{{ url_for('forum.topic', topic_id=topic.id, slug=topic.slug) }}"
class="block p-4 hover:bg-gray-50 transition-colors">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
{% if topic.is_pinned %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
Pinned
</span>
{% endif %}
{% if topic.is_locked %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
Locked
</span>
{% endif %}
<h3 class="font-medium text-gray-900 truncate">{{ topic.title }}</h3>
</div>
<div class="mt-1 text-sm text-gray-500">
by {{ topic.author_name }} &middot; {{ topic.created_at | timeago }}
</div>
</div>
<div class="ml-4 flex-shrink-0 text-right">
<div class="text-sm text-gray-900">{{ topic.reply_count }} replies</div>
<div class="text-xs text-gray-500">{{ topic.view_count }} views</div>
</div>
</div>
</a>
{% endfor %}
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<nav class="mt-6 flex items-center justify-center gap-2">
{% if page > 1 %}
<a href="{{ url_for('forum.category', slug=category.slug, page=page-1) }}"
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Previous
</a>
{% endif %}
<span class="px-3 py-2 text-sm text-gray-700">
Page {{ page }} of {{ total_pages }}
</span>
{% if page < total_pages %}
<a href="{{ url_for('forum.category', slug=category.slug, page=page+1) }}"
class="px-3 py-2 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Next
</a>
{% endif %}
</nav>
{% endif %}
{% else %}
<!-- Empty state -->
<div class="bg-white border border-gray-200 rounded-lg p-8 text-center">
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No topics yet</h3>
<p class="mt-2 text-gray-600">Be the first to start a discussion in this category.</p>
<a href="{{ url_for('forum.new_topic', slug=category.slug) }}"
class="mt-4 inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
Create the first topic
</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+136
View File
@@ -0,0 +1,136 @@
{% extends "base.html" %}
{% block title %}Community Forum{% endblock %}
{% block meta_description %}Join the CmdForge community. Ask questions, share your projects, and connect with other users.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Hero -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold text-gray-900">Community Forum</h1>
<p class="mt-2 text-gray-600">
Ask questions, share your projects, and connect with other CmdForge users.
</p>
<div class="mt-4 flex items-center gap-6 text-sm text-gray-500">
<span>{{ stats.topics }} topics</span>
<span>{{ stats.replies }} replies</span>
</div>
</div>
</div>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="lg:grid lg:grid-cols-3 lg:gap-8">
<!-- Categories -->
<div class="lg:col-span-2">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Categories</h2>
<div class="space-y-3">
{% for cat in categories %}
<a href="{{ url_for('forum.category', slug=cat.slug) }}"
class="block bg-white border border-gray-200 rounded-lg p-4 hover:border-indigo-300 hover:shadow-sm transition-all">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-3">
<!-- Icon -->
<div class="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center
{% if cat.icon == 'chat' %}bg-blue-100 text-blue-600
{% elif cat.icon == 'question' %}bg-amber-100 text-amber-600
{% elif cat.icon == 'star' %}bg-purple-100 text-purple-600
{% elif cat.icon == 'lightbulb' %}bg-green-100 text-green-600
{% elif cat.icon == 'book' %}bg-indigo-100 text-indigo-600
{% else %}bg-gray-100 text-gray-600{% endif %}">
{% if cat.icon == 'chat' %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
</svg>
{% elif cat.icon == 'question' %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{% elif cat.icon == 'star' %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
</svg>
{% elif cat.icon == 'lightbulb' %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{% elif cat.icon == 'book' %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
{% else %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
</svg>
{% endif %}
</div>
<div>
<h3 class="font-semibold text-gray-900">{{ cat.name }}</h3>
<p class="text-sm text-gray-600 mt-0.5">{{ cat.description }}</p>
</div>
</div>
<div class="text-right text-sm text-gray-500 flex-shrink-0 ml-4">
<div>{{ cat.topic_count }} topics</div>
<div class="text-xs">{{ cat.reply_count }} replies</div>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Sidebar -->
<div class="mt-8 lg:mt-0">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h2>
{% if recent_topics %}
<div class="bg-white border border-gray-200 rounded-lg divide-y divide-gray-100">
{% for topic in recent_topics %}
<a href="{{ url_for('forum.topic', topic_id=topic.id, slug=topic.slug) }}"
class="block p-3 hover:bg-gray-50 transition-colors">
<div class="font-medium text-gray-900 text-sm truncate">{{ topic.title }}</div>
<div class="text-xs text-gray-500 mt-1">
in {{ topic.category_name }} &middot; {{ topic.reply_count }} replies
</div>
</a>
{% endfor %}
</div>
{% else %}
<div class="bg-white border border-gray-200 rounded-lg p-4 text-center text-gray-500">
<p>No topics yet. Be the first to start a discussion!</p>
</div>
{% endif %}
<!-- Quick Links -->
<div class="mt-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Quick Links</h2>
<div class="space-y-2">
<a href="{{ url_for('web.docs', path='getting-started') }}"
class="flex items-center text-sm text-gray-600 hover:text-indigo-600">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Getting Started Guide
</a>
<a href="{{ url_for('web.tools') }}"
class="flex items-center text-sm text-gray-600 hover:text-indigo-600">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
Browse Tools
</a>
<a href="https://gitea.brrd.tech/rob/CmdForge/issues"
target="_blank" rel="noopener"
class="flex items-center text-sm text-gray-600 hover:text-indigo-600">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Report a Bug
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,123 @@
{% extends "base.html" %}
{% block title %}New Topic in {{ category.name }} - Forum{% endblock %}
{% block meta_description %}Start a new discussion in {{ category.name }}.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Breadcrumb -->
<nav class="text-sm text-gray-500 mb-4">
<a href="{{ url_for('forum.index') }}" class="hover:text-indigo-600">Forum</a>
<span class="mx-2">/</span>
<a href="{{ url_for('forum.category', slug=category.slug) }}" class="hover:text-indigo-600">{{ category.name }}</a>
<span class="mx-2">/</span>
<span class="text-gray-900">New Topic</span>
</nav>
<h1 class="text-2xl font-bold text-gray-900">New Topic in {{ category.name }}</h1>
</div>
</div>
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{% if error %}
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-md text-red-800">
{{ error }}
</div>
{% endif %}
<form method="POST" class="bg-white border border-gray-200 rounded-lg p-6">
<!-- Honeypot field (hidden, bots fill this) -->
<div style="display: none;">
<input type="text" name="website_url" tabindex="-1" autocomplete="off">
</div>
<!-- Author info -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<label for="author_name" class="block text-sm font-medium text-gray-700 mb-1">
Your Name <span class="text-red-500">*</span>
</label>
<input type="text"
id="author_name"
name="author_name"
required
value="{{ form_data.author_name if form_data else '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="John Doe">
</div>
<div>
<label for="author_email" class="block text-sm font-medium text-gray-700 mb-1">
Email <span class="text-gray-400">(optional)</span>
</label>
<input type="email"
id="author_email"
name="author_email"
value="{{ form_data.author_email if form_data else '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="[email protected]">
<p class="mt-1 text-xs text-gray-500">Used for Gravatar only, not displayed publicly.</p>
</div>
</div>
<!-- Title -->
<div class="mb-6">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">
Topic Title <span class="text-red-500">*</span>
</label>
<input type="text"
id="title"
name="title"
required
value="{{ form_data.title if form_data else '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="How do I...?">
<p class="mt-1 text-xs text-gray-500">Be specific and descriptive (5-200 characters).</p>
</div>
<!-- Content -->
<div class="mb-6">
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">
Content <span class="text-red-500">*</span>
</label>
<textarea id="content"
name="content"
rows="12"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
placeholder="Describe your question, idea, or project in detail...">{{ form_data.content if form_data else '' }}</textarea>
<p class="mt-1 text-xs text-gray-500">
Markdown is supported: **bold**, *italic*, `code`, ```code blocks```, [links](url)
</p>
</div>
<!-- Tips -->
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-md">
<h3 class="text-sm font-medium text-blue-800 mb-2">Tips for a great post:</h3>
<ul class="text-sm text-blue-700 space-y-1 list-disc list-inside">
<li>Search first to avoid duplicate topics</li>
<li>Use a clear, descriptive title</li>
<li>Include relevant details and context</li>
<li>For bugs, include steps to reproduce</li>
<li>Format code with triple backticks: ```code```</li>
</ul>
</div>
<!-- Submit -->
<div class="flex items-center justify-between">
<a href="{{ url_for('forum.category', slug=category.slug) }}"
class="text-gray-600 hover:text-gray-900">
Cancel
</a>
<button type="submit"
class="px-6 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Create Topic
</button>
</div>
</form>
</div>
</div>
{% endblock %}
+178
View File
@@ -0,0 +1,178 @@
{% extends "base.html" %}
{% block title %}{{ topic.title }} - Forum{% endblock %}
{% block meta_description %}{{ topic.content[:160] }}{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Breadcrumb -->
<nav class="text-sm text-gray-500 mb-4">
<a href="{{ url_for('forum.index') }}" class="hover:text-indigo-600">Forum</a>
<span class="mx-2">/</span>
{% if category %}
<a href="{{ url_for('forum.category', slug=category.slug) }}" class="hover:text-indigo-600">{{ category.name }}</a>
<span class="mx-2">/</span>
{% endif %}
<span class="text-gray-900 truncate">{{ topic.title[:50] }}{% if topic.title|length > 50 %}...{% endif %}</span>
</nav>
<div class="flex items-start justify-between">
<div>
<div class="flex items-center gap-2 flex-wrap">
{% if topic.is_pinned %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">
Pinned
</span>
{% endif %}
{% if topic.is_locked %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
Locked
</span>
{% endif %}
<h1 class="text-2xl font-bold text-gray-900">{{ topic.title }}</h1>
</div>
<p class="mt-2 text-sm text-gray-500">
{{ topic.view_count }} views &middot; {{ replies|length }} replies
</p>
</div>
</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 p-4 rounded-md {% if category == 'error' %}bg-red-50 text-red-800{% else %}bg-green-50 text-green-800{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Original post -->
<article class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div class="p-6">
<div class="flex items-center mb-4">
<!-- Avatar placeholder -->
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 font-semibold">
{{ topic.author_name[0]|upper }}
</div>
<div class="ml-3">
<div class="font-medium text-gray-900">{{ topic.author_name }}</div>
<div class="text-sm text-gray-500">{{ topic.created_at | timeago }}</div>
</div>
</div>
<div class="prose prose-sm max-w-none">
{{ topic.content | markdown | safe }}
</div>
</div>
</article>
<!-- Replies -->
{% if replies %}
<div class="mt-6 space-y-4">
<h2 class="text-lg font-semibold text-gray-900">{{ replies|length }} {% if replies|length == 1 %}Reply{% else %}Replies{% endif %}</h2>
{% for reply in replies %}
<article class="bg-white border border-gray-200 rounded-lg overflow-hidden {% if reply.is_solution %}ring-2 ring-green-500{% endif %}" id="reply-{{ reply.id }}">
<div class="p-6">
<div class="flex items-center mb-4">
<!-- Avatar placeholder -->
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-600 font-semibold">
{{ reply.author_name[0]|upper }}
</div>
<div class="ml-3 flex-1">
<div class="flex items-center gap-2">
<span class="font-medium text-gray-900">{{ reply.author_name }}</span>
{% if reply.is_solution %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
Solution
</span>
{% endif %}
</div>
<div class="text-sm text-gray-500">{{ reply.created_at | timeago }}</div>
</div>
</div>
<div class="prose prose-sm max-w-none">
{{ reply.content | markdown | safe }}
</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
<!-- Reply form -->
{% if not topic.is_locked %}
<div class="mt-8">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Post a Reply</h2>
<form method="POST" action="{{ url_for('forum.reply', topic_id=topic.id) }}" class="bg-white border border-gray-200 rounded-lg p-6">
<!-- Honeypot field (hidden, bots fill this) -->
<div style="display: none;">
<input type="text" name="website_url" tabindex="-1" autocomplete="off">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="author_name" class="block text-sm font-medium text-gray-700 mb-1">
Your Name <span class="text-red-500">*</span>
</label>
<input type="text"
id="author_name"
name="author_name"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="John Doe">
</div>
<div>
<label for="author_email" class="block text-sm font-medium text-gray-700 mb-1">
Email <span class="text-gray-400">(optional)</span>
</label>
<input type="email"
id="author_email"
name="author_email"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
placeholder="[email protected]">
</div>
</div>
<div class="mb-4">
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">
Your Reply <span class="text-red-500">*</span>
</label>
<textarea id="content"
name="content"
rows="6"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
placeholder="Write your reply here... (Markdown supported)"></textarea>
<p class="mt-1 text-xs text-gray-500">
Markdown is supported: **bold**, *italic*, `code`, ```code blocks```, [links](url)
</p>
</div>
<div class="flex justify-end">
<button type="submit"
class="px-4 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Post Reply
</button>
</div>
</form>
</div>
{% else %}
<div class="mt-8 bg-gray-100 border border-gray-200 rounded-lg p-6 text-center">
<svg class="mx-auto w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
<p class="mt-2 text-gray-600">This topic is locked and cannot receive new replies.</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+138
View File
@@ -0,0 +1,138 @@
{% extends "base.html" %}
{% block title %}About - CmdForge{% endblock %}
{% block meta_description %}Learn about CmdForge, the open-source platform for building custom AI-powered command-line tools.{% endblock %}
{% block content %}
<div class="bg-white">
<!-- Hero -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 text-center">
<h1 class="text-4xl font-bold text-gray-900">About CmdForge</h1>
<p class="mt-6 text-xl text-gray-600">
An open-source platform for building and sharing AI-powered command-line tools.
</p>
</div>
<!-- Mission -->
<div class="bg-gray-50 py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Our Mission</h2>
<div class="prose prose-lg prose-indigo max-w-none">
<p>
CmdForge was created with a simple belief: powerful AI tools should be accessible to everyone,
not just those with extensive programming knowledge or expensive API subscriptions.
</p>
<p>
We're building a <strong>universally accessible development ecosystem</strong> that empowers
regular people to collaborate and build upon each other's progress rather than compete.
</p>
<p>
Our platform follows the Unix philosophy: simple, composable tools that do one thing well.
With CmdForge, you can create custom AI commands using simple YAML configuration,
chain them together, and share them with the community.
</p>
</div>
</div>
</div>
<!-- Values -->
<div class="py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-2xl font-bold text-gray-900 mb-8">Our Values</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="bg-white border border-gray-200 rounded-lg p-6">
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Open Source</h3>
<p class="text-gray-600">
CmdForge is MIT licensed and open source. We believe in transparency and community ownership.
</p>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-6">
<div class="w-12 h-12 bg-cyan-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Community First</h3>
<p class="text-gray-600">
We prioritize collaboration over competition. Share tools, learn from others, build together.
</p>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-6">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Privacy Respecting</h3>
<p class="text-gray-600">
Your data stays yours. We collect minimal analytics and never sell user information.
</p>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-6">
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Provider Agnostic</h3>
<p class="text-gray-600">
Works with any AI provider. Use Claude, GPT, local models, or any CLI-accessible AI.
</p>
</div>
</div>
</div>
</div>
<!-- Sustainability -->
<div class="bg-gray-50 py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Sustainability</h2>
<div class="prose prose-lg prose-indigo max-w-none">
<p>
CmdForge is committed to long-term sustainability. Revenue from optional ads
and donations supports:
</p>
<ul>
<li>Maintaining and expanding the project</li>
<li>Hosting infrastructure for the registry</li>
<li>Future hosting of AI models for users with less access to paid services</li>
<li>Building a sustainable, community-first platform</li>
</ul>
<p>
<a href="{{ url_for('web.donate') }}" class="text-indigo-600 hover:text-indigo-800">
Support the project
</a>
</p>
</div>
</div>
</div>
<!-- Open Source -->
<div class="py-16">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Contribute</h2>
<p class="text-lg text-gray-600 mb-8">
CmdForge is open source and welcomes contributions of all kinds.
</p>
<a href="https://github.com/your-org/cmdforge"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center px-6 py-3 text-base font-medium text-white bg-gray-900 rounded-md hover:bg-gray-800">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"/>
</svg>
View on GitHub
</a>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,183 @@
{% extends "base.html" %}
{% block title %}Community - CmdForge{% endblock %}
{% block meta_description %}Connect with CmdForge - report issues, request features, and contribute to the project.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Hero -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
<h1 class="text-3xl font-bold text-gray-900">Community</h1>
<p class="mt-4 text-lg text-gray-600">
CmdForge is open source. Get help, report issues, and contribute.
</p>
</div>
</div>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Forum CTA -->
<section class="mb-12">
<a href="{{ url_for('forum.index') }}"
class="block bg-indigo-600 rounded-lg p-6 hover:bg-indigo-700 transition-colors">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-indigo-500 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
</svg>
</div>
<div>
<h2 class="text-xl font-bold text-white">Community Forum</h2>
<p class="text-indigo-100">Ask questions, share projects, and connect with other users</p>
</div>
</div>
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</div>
</a>
</section>
<!-- Community hubs -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Source Code</h2>
<p class="mt-2 text-sm text-gray-600">
Browse the source, fork the project, and submit pull requests.
</p>
<a href="https://gitea.brrd.tech/rob/CmdForge"
target="_blank" rel="noopener"
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
View Repository
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Report Issues</h2>
<p class="mt-2 text-sm text-gray-600">
Found a bug? Have a feature request? Open an issue.
</p>
<a href="https://gitea.brrd.tech/rob/CmdForge/issues"
target="_blank" rel="noopener"
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
Open Issue
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
</a>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Documentation</h2>
<p class="mt-2 text-sm text-gray-600">
Learn how to build tools, configure providers, and publish to the registry.
</p>
<a href="{{ url_for('web.docs', path='getting-started') }}"
class="mt-4 inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-800">
Read the Docs
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
</div>
</section>
<!-- Get Involved -->
<section class="bg-white rounded-lg border border-gray-200 p-8">
<h2 class="text-2xl font-bold text-gray-900 text-center">Ways to Contribute</h2>
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<span class="text-indigo-600 font-bold">1</span>
</div>
<div>
<h3 class="font-semibold text-gray-900">Share Your Tools</h3>
<p class="mt-1 text-sm text-gray-600">
Built something useful? <a href="{{ url_for('web.docs', path='publishing') }}" class="text-indigo-600 hover:underline">Publish it to the registry</a> so others can use it.
</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<span class="text-indigo-600 font-bold">2</span>
</div>
<div>
<h3 class="font-semibold text-gray-900">Improve the Docs</h3>
<p class="mt-1 text-sm text-gray-600">
Found something confusing? Submit a PR to improve the documentation.
</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<span class="text-indigo-600 font-bold">3</span>
</div>
<div>
<h3 class="font-semibold text-gray-900">Report Bugs</h3>
<p class="mt-1 text-sm text-gray-600">
Help make CmdForge better by reporting issues with clear reproduction steps.
</p>
</div>
</div>
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
<span class="text-indigo-600 font-bold">4</span>
</div>
<div>
<h3 class="font-semibold text-gray-900">Add Features</h3>
<p class="mt-1 text-sm text-gray-600">
Check the issues for feature requests or propose your own ideas.
</p>
</div>
</div>
</div>
</section>
<!-- Quick Links -->
<section class="mt-8 text-center">
<div class="flex flex-wrap items-center justify-center gap-4">
<a href="{{ url_for('forum.index') }}"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Visit Forum
</a>
<a href="https://gitea.brrd.tech/rob/CmdForge#installation"
target="_blank" rel="noopener"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Installation
</a>
<a href="{{ url_for('web.tools') }}"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Browse Tools
</a>
<a href="{{ url_for('web.tutorials') }}"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Tutorials
</a>
</div>
</section>
</div>
</div>
{% endblock %}
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}{{ title }} - CmdForge{% endblock %}
{% block content %}
<section class="bg-white py-16">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold text-gray-900">{{ title }}</h1>
<p class="mt-4 text-gray-600">{{ body }}</p>
</div>
</section>
{% endblock %}
+208
View File
@@ -0,0 +1,208 @@
{% extends "base.html" %}
{% from "components/callouts.html" import info, warning, tip %}
{% block title %}{{ page.title }} - CmdForge Docs{% endblock %}
{% block meta_description %}{{ page.description or page.title }}{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="lg:grid lg:grid-cols-12 lg:gap-8">
<!-- Left Sidebar - TOC -->
<aside class="hidden lg:block lg:col-span-3">
<nav class="sticky top-4 bg-white rounded-lg border border-gray-200 p-4 max-h-[calc(100vh-6rem)] overflow-y-auto">
<h3 class="text-sm font-semibold text-gray-900 mb-4">Documentation</h3>
<ul class="space-y-1">
{% for section in toc %}
<li>
<a href="{{ url_for('web.docs', path=section.slug) }}"
class="block px-3 py-2 text-sm rounded-md {{ 'bg-indigo-50 text-indigo-700 font-medium' if current_path == section.slug else 'text-gray-600 hover:bg-gray-50' }}">
{{ section.title }}
</a>
{% if section.children %}
<ul class="ml-4 mt-1 space-y-1">
{% for child in section.children %}
<li>
<a href="{{ url_for('web.docs', path=child.slug) }}"
class="block px-3 py-1.5 text-sm rounded-md {{ 'bg-indigo-50 text-indigo-700 font-medium' if current_path == child.slug else 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' }}">
{{ child.title }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
</aside>
<!-- Main Content -->
<main class="lg:col-span-6">
<!-- Mobile TOC Toggle -->
<div class="lg:hidden mb-6">
<button type="button"
onclick="toggleMobileToc()"
class="w-full flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-lg">
<span class="font-medium text-gray-700">On this page</span>
<svg id="toc-chevron" class="w-5 h-5 text-gray-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<nav id="mobile-toc" class="hidden mt-2 bg-white border border-gray-200 rounded-lg p-4">
<ul class="space-y-2">
{% for section in toc %}
<li>
<a href="{{ url_for('web.docs', path=section.slug) }}"
class="block text-sm {{ 'text-indigo-600 font-medium' if current_path == section.slug else 'text-gray-600' }}">
{{ section.title }}
</a>
</li>
{% endfor %}
</ul>
</nav>
</div>
<!-- Breadcrumb -->
<nav class="mb-6 text-sm text-gray-500" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2">
<li><a href="{{ url_for('web.docs', path='') }}" class="hover:text-gray-700">Docs</a></li>
{% if page.parent %}
<li class="flex items-center">
<svg class="w-4 h-4 mx-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
<a href="{{ url_for('web.docs', path=page.parent.slug) }}" class="hover:text-gray-700">{{ page.parent.title }}</a>
</li>
{% endif %}
<li class="flex items-center">
<svg class="w-4 h-4 mx-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
<span class="text-gray-900 font-medium">{{ page.title }}</span>
</li>
</ol>
</nav>
<!-- Article Content -->
<article class="bg-white rounded-lg border border-gray-200 p-8">
<h1 class="text-3xl font-bold text-gray-900 mb-6">{{ page.title }}</h1>
<div class="prose prose-lg prose-indigo max-w-none">
{{ page.content_html|safe }}
</div>
<!-- Page Navigation -->
<div class="mt-12 pt-8 border-t border-gray-200 flex items-center justify-between">
{% if prev_page %}
<a href="{{ url_for('web.docs', path=prev_page.slug) }}"
class="flex items-center text-indigo-600 hover:text-indigo-800">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
{{ prev_page.title }}
</a>
{% else %}
<div></div>
{% endif %}
{% if next_page %}
<a href="{{ url_for('web.docs', path=next_page.slug) }}"
class="flex items-center text-indigo-600 hover:text-indigo-800">
{{ next_page.title }}
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
{% endif %}
</div>
</article>
<!-- Edit on GitHub -->
<div class="mt-6 text-center">
<a href="https://github.com/your-org/cmdforge/edit/main/docs/{{ current_path }}.md"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center text-sm text-gray-500 hover:text-gray-700">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
</svg>
Edit this page on GitHub
</a>
</div>
</main>
<!-- Right Sidebar - On This Page + Ads -->
<aside class="hidden lg:block lg:col-span-3">
<div class="sticky top-4 space-y-6">
<!-- On This Page -->
{% if page.headings %}
<nav class="bg-white rounded-lg border border-gray-200 p-4">
<h3 class="text-sm font-semibold text-gray-900 mb-3">On this page</h3>
<ul class="space-y-2 text-sm" id="page-toc">
{% for heading in page.headings %}
<li class="{{ 'ml-4' if heading.level > 2 else '' }}">
<a href="#{{ heading.id }}"
class="text-gray-500 hover:text-gray-900 transition-colors"
data-heading="{{ heading.id }}">
{{ heading.text }}
</a>
</li>
{% endfor %}
</ul>
</nav>
{% endif %}
<!-- Ad Zone -->
{% if show_ads %}
<div class="bg-blue-50 rounded-lg border border-blue-100 p-4">
<p class="text-xs text-gray-500 mb-2">Advertisement</p>
<div id="docs-sidebar-ad" class="min-h-[250px]">
<!-- Ad content -->
</div>
</div>
{% endif %}
</div>
</aside>
</div>
</div>
</div>
<script>
function toggleMobileToc() {
const toc = document.getElementById('mobile-toc');
const chevron = document.getElementById('toc-chevron');
toc.classList.toggle('hidden');
chevron.classList.toggle('rotate-180');
}
// Scroll spy for page TOC
document.addEventListener('DOMContentLoaded', function() {
const headings = document.querySelectorAll('article h2, article h3');
const tocLinks = document.querySelectorAll('#page-toc a');
if (headings.length === 0 || tocLinks.length === 0) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
tocLinks.forEach(link => {
link.classList.remove('text-indigo-600', 'font-medium');
link.classList.add('text-gray-500');
if (link.dataset.heading === entry.target.id) {
link.classList.remove('text-gray-500');
link.classList.add('text-indigo-600', 'font-medium');
}
});
}
});
}, {rootMargin: '-100px 0px -66%'});
headings.forEach(heading => observer.observe(heading));
});
</script>
{% endblock %}
@@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}Support CmdForge{% endblock %}
{% block meta_description %}Support CmdForge by contributing code, tools, or feedback.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Hero -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
<h1 class="text-3xl font-bold text-gray-900">Support CmdForge</h1>
<p class="mt-4 text-lg text-gray-600">
CmdForge is a passion project. The best way to support it is to use it and share what you build.
</p>
</div>
</div>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Ways to help -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Build Tools</h2>
<p class="mt-2 text-sm text-gray-600">
Create and publish useful tools to the registry. A growing library helps everyone.
</p>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Spread the Word</h2>
<p class="mt-2 text-sm text-gray-600">
Tell a friend or colleague who might find CmdForge useful.
</p>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
</svg>
</div>
<h2 class="text-lg font-semibold text-gray-900">Give Feedback</h2>
<p class="mt-2 text-sm text-gray-600">
Report bugs, suggest features, or just share what you're building with it.
</p>
</div>
</section>
<!-- Main CTA -->
<section class="bg-white rounded-lg border border-gray-200 p-8 text-center">
<h2 class="text-2xl font-bold text-gray-900">Get Involved</h2>
<p class="mt-3 text-gray-600 max-w-2xl mx-auto">
CmdForge is open source. The code, docs, and registry are all open for contributions.
</p>
<div class="mt-6 flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="https://gitea.brrd.tech/rob/CmdForge"
target="_blank" rel="noopener"
class="inline-flex items-center px-6 py-3 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
View on Gitea
</a>
<a href="{{ url_for('web.docs', path='publishing') }}"
class="inline-flex items-center px-6 py-3 text-sm font-medium text-indigo-600 border border-indigo-600 rounded-md hover:bg-indigo-50">
Publish a Tool
</a>
</div>
</section>
<!-- Thank you -->
<section class="mt-12 text-center">
<p class="text-gray-500">
Thanks for using CmdForge!
</p>
</section>
</div>
</div>
{% endblock %}
+200
View File
@@ -0,0 +1,200 @@
{% extends "base.html" %}
{% from "components/tool_card.html" import tool_card %}
{% from "components/tutorial_card.html" import tutorial_card %}
{% from "components/contributor_card.html" import contributor_card %}
{% block title %}CmdForge - Build Custom AI Commands in YAML{% endblock %}
{% block meta_description %}Create Unix-style pipeable AI tools with simple YAML configuration. Provider-agnostic, composable, and community-driven.{% endblock %}
{% block content %}
<!-- Hero Section -->
<section class="bg-white py-16 md:py-24">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 leading-tight">
Build Custom AI Commands in YAML
</h1>
<p class="mt-6 text-xl text-gray-600 max-w-3xl mx-auto">
Create Unix-style pipeable tools that work with any AI provider.
Provider-agnostic and composable for ultimate flexibility.
</p>
<!-- Install Link -->
<div class="mt-10">
<a href="https://gitea.brrd.tech/rob/CmdForge#installation"
target="_blank"
rel="noopener"
class="inline-flex items-center gap-2 text-indigo-600 hover:text-indigo-800 font-medium transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
View Installation Options (pip, Docker, 60-second setup)
</a>
</div>
<!-- CTAs -->
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="{{ url_for('web.docs', path='getting-started') }}"
class="w-full sm:w-auto inline-flex justify-center items-center px-8 py-4 text-lg font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 transition-colors shadow-md">
Get Started
</a>
<a href="{{ url_for('web.tutorials') }}"
class="w-full sm:w-auto inline-flex justify-center items-center px-8 py-4 text-lg font-medium text-cyan-700 border-2 border-cyan-600 rounded-md hover:bg-cyan-50 transition-colors">
View Tutorials
</a>
</div>
</div>
</section>
<!-- Three Pillars Section -->
<section class="py-16 bg-gray-50">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12">
Why CmdForge?
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Pillar 1: Easy -->
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center hover:shadow-lg transition-shadow">
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Easy to Use</h3>
<p class="text-gray-600">
Simple YAML configuration for quick setup. No complex programming required to get started.
</p>
</div>
<!-- Pillar 2: Powerful -->
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center hover:shadow-lg transition-shadow">
<div class="w-16 h-16 bg-cyan-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Powerful</h3>
<p class="text-gray-600">
Leverage any AI provider, compose complex multi-step workflows with Python code integration.
</p>
</div>
<!-- Pillar 3: Community -->
<div class="bg-white rounded-lg border border-gray-200 p-8 text-center hover:shadow-lg transition-shadow">
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-8 h-8 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-3">Community</h3>
<p class="text-gray-600">
Share, discover, and contribute to a growing ecosystem of tools built by developers like you.
</p>
</div>
</div>
</div>
</section>
<!-- Featured Tools Section -->
<section class="py-16 bg-white">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-12">
<h2 class="text-3xl font-bold text-gray-900">
Featured Tools & Projects
</h2>
<a href="{{ url_for('web.tools') }}" class="text-indigo-600 hover:text-indigo-800 font-medium flex items-center">
View All
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for tool in featured_tools %}
{{ tool_card(
owner=tool.owner,
name=tool.name,
description=tool.description,
category=tool.category,
downloads=tool.downloads,
version=tool.version
) }}
{% else %}
<!-- Placeholder when no tools -->
<div class="col-span-full text-center py-12 text-gray-500">
<p class="mb-4">No featured tools yet. Be the first to publish!</p>
<a href="{{ url_for('web.docs', path='publishing') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
Learn how to publish a tool
</a>
</div>
{% endfor %}
</div>
</div>
</section>
<!-- Getting Started Section -->
<section class="py-16 bg-gray-50">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12">
Getting Started
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{{ tutorial_card(
title="Basic Setup",
description="Learn how to install CmdForge and configure your first AI provider.",
href=url_for('web.docs', path='getting-started'),
step_number=1
) }}
{{ tutorial_card(
title="Your First Tool",
description="Create a simple AI-powered command that you can use from your terminal.",
href=url_for('web.tutorials_path', path='first-tool'),
step_number=2
) }}
{{ tutorial_card(
title="Advanced Workflows",
description="Combine multiple steps and providers to build powerful automation.",
href=url_for('web.tutorials_path', path='advanced-workflows'),
step_number=3
) }}
</div>
</div>
</section>
<!-- Featured Contributor Section -->
{% if featured_contributor %}
<section class="py-16 bg-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 class="text-3xl font-bold text-gray-900 text-center mb-12">
Featured Contributor
</h2>
{{ contributor_card(
display_name=featured_contributor.display_name,
slug=featured_contributor.slug,
bio=featured_contributor.bio_override or featured_contributor.bio,
verified=featured_contributor.verified
) }}
</div>
</section>
{% endif %}
<!-- Ad Zone (Optional) -->
{% if show_ads %}
<section class="py-8 bg-blue-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p class="text-xs text-gray-500 mb-2">Advertisement</p>
<div id="footer-ad-zone" class="min-h-[90px] flex items-center justify-center">
<!-- Ad content loaded dynamically -->
<p class="text-gray-400 text-sm">Support CmdForge Development</p>
</div>
</div>
</section>
{% endif %}
{% endblock %}
@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% from "components/forms.html" import text_input, button_primary, form_errors %}
{% block title %}Sign In - CmdForge{% endblock %}
{% block content %}
<div class="min-h-[70vh] flex items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<a href="{{ url_for('web.home') }}" class="inline-flex items-center justify-center">
<svg class="w-10 h-10 text-indigo-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</a>
<h1 class="mt-4 text-2xl font-bold text-gray-900">Sign in to CmdForge</h1>
<p class="mt-2 text-gray-600">
Access your dashboard and manage your tools
</p>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
{{ form_errors(errors) }}
<form action="{{ url_for('web.login') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if next_url %}
<input type="hidden" name="next" value="{{ next_url }}">
{% endif %}
{{ text_input(
name='email',
label='Email address',
type='email',
placeholder='[email protected]',
required=true,
value=email or ''
) }}
{{ text_input(
name='password',
label='Password',
type='password',
placeholder='Your password',
required=true
) }}
<div class="flex items-center justify-between mb-6">
<div class="flex items-center">
<input type="checkbox"
name="remember"
id="remember"
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="remember" class="ml-2 text-sm text-gray-600">
Remember me
</label>
</div>
<a href="{{ url_for('web.forgot_password') }}" class="text-sm text-indigo-600 hover:text-indigo-800">
Forgot password?
</a>
</div>
{{ button_primary('Sign in', full_width=true) }}
</form>
</div>
<p class="mt-6 text-center text-sm text-gray-600">
Don't have an account?
<a href="{{ url_for('web.register') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
Create one
</a>
</p>
</div>
</div>
{% endblock %}
@@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block title %}Privacy Policy - CmdForge{% endblock %}
{% block meta_description %}CmdForge privacy policy. Learn how we collect, use, and protect your data.{% endblock %}
{% block content %}
<div class="bg-white py-16">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Privacy Policy</h1>
<p class="text-gray-500 mb-8">Last updated: January 2025</p>
<div class="prose prose-lg prose-indigo max-w-none">
<h2>Introduction</h2>
<p>
CmdForge ("we", "our", or "us") respects your privacy and is committed to protecting
your personal data. This privacy policy explains how we collect, use, and safeguard
your information when you use our website and services.
</p>
<h2>Information We Collect</h2>
<h3>Account Information</h3>
<p>When you create an account, we collect:</p>
<ul>
<li>Email address</li>
<li>Username (slug)</li>
<li>Display name</li>
<li>Password (stored securely hashed)</li>
<li>Optional: bio and website URL</li>
</ul>
<h3>Usage Data</h3>
<p>We automatically collect certain information when you use our services:</p>
<ul>
<li>Tool download counts (anonymized)</li>
<li>Page views and navigation patterns (if analytics consent given)</li>
<li>Browser type and version</li>
<li>IP address (for security and rate limiting)</li>
</ul>
<h3>Cookies</h3>
<p>We use cookies for:</p>
<ul>
<li><strong>Essential cookies:</strong> Required for the website to function (session management, CSRF protection)</li>
<li><strong>Analytics cookies:</strong> Help us understand how visitors use our site (optional, requires consent)</li>
<li><strong>Advertising cookies:</strong> Used to show relevant ads (optional, requires consent)</li>
</ul>
<p>
You can manage your cookie preferences at any time through our cookie consent banner
or by contacting us.
</p>
<h2>How We Use Your Information</h2>
<p>We use your information to:</p>
<ul>
<li>Provide and maintain our services</li>
<li>Authenticate your identity and secure your account</li>
<li>Display your public profile and published tools</li>
<li>Send important service notifications</li>
<li>Improve our services based on usage patterns</li>
<li>Prevent abuse and enforce our terms of service</li>
</ul>
<h2>Information Sharing</h2>
<p>We do not sell your personal information. We may share data with:</p>
<ul>
<li><strong>Service providers:</strong> Hosting, analytics, and email services that help us operate</li>
<li><strong>Legal requirements:</strong> When required by law or to protect our rights</li>
<li><strong>Public information:</strong> Your username, display name, bio, and published tools are publicly visible</li>
</ul>
<h2>Data Security</h2>
<p>
We implement appropriate security measures to protect your personal information,
including:
</p>
<ul>
<li>Encryption of data in transit (HTTPS)</li>
<li>Secure password hashing (bcrypt)</li>
<li>Regular security audits</li>
<li>Limited access to personal data</li>
</ul>
<h2>Your Rights</h2>
<p>You have the right to:</p>
<ul>
<li><strong>Access:</strong> Request a copy of your personal data</li>
<li><strong>Correction:</strong> Update inaccurate information in your account settings</li>
<li><strong>Deletion:</strong> Delete your account and associated data</li>
<li><strong>Portability:</strong> Export your data in a machine-readable format</li>
<li><strong>Withdraw consent:</strong> Opt out of analytics and advertising cookies</li>
</ul>
<h2>Data Retention</h2>
<p>
We retain your account data for as long as your account is active. If you delete
your account, we will remove your personal data within 30 days, except where we
are required to retain it for legal purposes.
</p>
<p>
Published tools may remain in the registry after account deletion for continuity,
but will be disassociated from your personal information.
</p>
<h2>Children's Privacy</h2>
<p>
Our services are not intended for children under 13. We do not knowingly collect
personal information from children under 13.
</p>
<h2>Changes to This Policy</h2>
<p>
We may update this privacy policy from time to time. We will notify you of any
significant changes by posting a notice on our website or sending you an email.
</p>
<h2>Contact Us</h2>
<p>
If you have questions about this privacy policy or your personal data, please
contact us at:
</p>
<ul>
<li>Email: [email protected]</li>
<li>GitHub: <a href="https://github.com/your-org/cmdforge/issues">Open an issue</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% from "components/tool_card.html" import tool_card %}
{% block title %}{{ publisher.display_name }} (@{{ publisher.slug }}) - CmdForge{% endblock %}
{% block meta_description %}{{ publisher.bio or 'CmdForge publisher profile for ' ~ publisher.display_name }}{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Profile Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="flex items-start gap-6">
<!-- Avatar -->
<div class="w-24 h-24 bg-indigo-100 rounded-full flex items-center justify-center flex-shrink-0">
<span class="text-3xl font-bold text-indigo-600">{{ publisher.display_name[0]|upper }}</span>
</div>
<!-- Info -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-gray-900 truncate">{{ publisher.display_name }}</h1>
{% if publisher.verified %}
<svg class="w-6 h-6 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20" title="Verified publisher">
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
{% endif %}
</div>
<p class="mt-1 text-gray-500">@{{ publisher.slug }}</p>
{% if publisher.bio %}
<p class="mt-4 text-gray-600">{{ publisher.bio }}</p>
{% endif %}
<div class="mt-4 flex items-center gap-4 text-sm text-gray-500">
{% if publisher.website %}
<a href="{{ publisher.website }}"
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-1 hover:text-indigo-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
Website
</a>
{% endif %}
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
{{ tools|length }} tool{{ 's' if tools|length != 1 else '' }}
</span>
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Joined {{ publisher.created_at|date_format }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Tools Section -->
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h2 class="text-xl font-semibold text-gray-900 mb-6">Published Tools</h2>
{% if tools %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{% for tool in tools %}
{{ tool_card(
owner=tool.owner,
name=tool.name,
description=tool.description,
category=tool.category,
downloads=tool.downloads,
version=tool.version
) }}
{% endfor %}
</div>
{% else %}
<div class="text-center py-12 bg-white rounded-lg border border-gray-200">
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<p class="mt-4 text-gray-600">No tools published yet.</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,101 @@
{% extends "base.html" %}
{% from "components/forms.html" import text_input, button_primary, form_errors, checkbox %}
{% block title %}Create Account - CmdForge{% endblock %}
{% block content %}
<div class="min-h-[70vh] flex items-center justify-center px-4 py-12">
<div class="w-full max-w-md">
<div class="text-center mb-8">
<a href="{{ url_for('web.home') }}" class="inline-flex items-center justify-center">
<svg class="w-10 h-10 text-indigo-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</a>
<h1 class="mt-4 text-2xl font-bold text-gray-900">Create your account</h1>
<p class="mt-2 text-gray-600">
Start publishing tools and join the community
</p>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
{{ form_errors(errors) }}
<form action="{{ url_for('web.register') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{{ text_input(
name='email',
label='Email address',
type='email',
placeholder='[email protected]',
required=true,
value=email or '',
help='We\'ll send a verification email to this address'
) }}
{{ text_input(
name='slug',
label='Username',
type='text',
placeholder='your-username',
required=true,
value=slug or '',
help='This will be your unique identifier (e.g., your-username/tool-name)'
) }}
{{ text_input(
name='display_name',
label='Display name',
type='text',
placeholder='Your Name',
required=true,
value=display_name or ''
) }}
{{ text_input(
name='password',
label='Password',
type='password',
placeholder='At least 8 characters',
required=true,
help='Use a mix of letters, numbers, and symbols'
) }}
{{ text_input(
name='password_confirm',
label='Confirm password',
type='password',
placeholder='Confirm your password',
required=true
) }}
<div class="mb-6">
<div class="flex items-start">
<input type="checkbox"
name="terms"
id="terms"
required
class="mt-1 h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="terms" class="ml-3 text-sm text-gray-600">
I agree to the
<a href="{{ url_for('web.terms') }}" class="text-indigo-600 hover:text-indigo-800">Terms of Service</a>
and
<a href="{{ url_for('web.privacy') }}" class="text-indigo-600 hover:text-indigo-800">Privacy Policy</a>
</label>
</div>
</div>
{{ button_primary('Create account', full_width=true) }}
</form>
</div>
<p class="mt-6 text-center text-sm text-gray-600">
Already have an account?
<a href="{{ url_for('web.login') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
Sign in
</a>
</p>
</div>
</div>
{% endblock %}
@@ -0,0 +1,143 @@
{% extends "base.html" %}
{% from "components/tool_card.html" import tool_card %}
{% block title %}Search: {{ query }} - CmdForge Registry{% endblock %}
{% block meta_description %}Search results for "{{ query }}" in the CmdForge Registry.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Search Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<form action="{{ url_for('web.search') }}" method="GET" class="max-w-2xl mx-auto">
<div class="relative">
<input type="text"
name="q"
value="{{ query or '' }}"
placeholder="Search for tools..."
aria-label="Search for tools"
autofocus
class="w-full pl-12 pr-4 py-4 text-lg border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<svg class="absolute left-4 top-4.5 w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<button type="submit"
class="absolute right-2 top-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Search
</button>
</div>
</form>
</div>
</div>
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{% if query %}
<!-- Results Count -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">
{% if results|length == 1 %}
1 result for "{{ query }}"
{% else %}
{{ results|length }} results for "{{ query }}"
{% endif %}
</h1>
</div>
{% if results %}
<!-- Search Results -->
<div class="space-y-4">
{% for tool in results %}
{{ tool_card(
owner=tool.owner,
name=tool.name,
description=tool.description,
category=tool.category,
downloads=tool.downloads,
version=tool.version
) }}
{% endfor %}
</div>
<!-- Pagination -->
{% if pagination and pagination.pages > 1 %}
<nav class="mt-12 flex items-center justify-center" aria-label="Pagination">
<div class="flex items-center gap-2">
{% if pagination.has_prev %}
<a href="{{ url_for('web.search', q=query, page=pagination.prev_num) }}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Previous
</a>
{% endif %}
<span class="px-4 py-2 text-sm text-gray-700">
Page {{ pagination.page }} of {{ pagination.pages }}
</span>
{% if pagination.has_next %}
<a href="{{ url_for('web.search', q=query, page=pagination.next_num) }}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Next
</a>
{% endif %}
</div>
</nav>
{% endif %}
{% else %}
<!-- No Results -->
<div class="text-center py-16">
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<h2 class="mt-4 text-lg font-medium text-gray-900">No results found</h2>
<p class="mt-2 text-gray-600">
We couldn't find any tools matching "{{ query }}".
</p>
<div class="mt-8">
<h3 class="text-sm font-medium text-gray-700 mb-4">Suggestions:</h3>
<ul class="text-sm text-gray-600 space-y-2">
<li>Check your spelling</li>
<li>Try more general keywords</li>
<li>Use fewer keywords</li>
</ul>
</div>
<div class="mt-8">
<a href="{{ url_for('web.tools') }}"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-indigo-600 border border-indigo-600 rounded-md hover:bg-indigo-50">
Browse All Tools
</a>
</div>
</div>
{% endif %}
{% else %}
<!-- Initial Search State -->
<div class="text-center py-16">
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<h2 class="mt-4 text-lg font-medium text-gray-900">Search the Registry</h2>
<p class="mt-2 text-gray-600">
Find tools by name, description, category, or tags.
</p>
<!-- Popular Categories -->
<div class="mt-8">
<h3 class="text-sm font-medium text-gray-700 mb-4">Popular Categories:</h3>
<div class="flex flex-wrap justify-center gap-2">
{% for cat in popular_categories %}
<a href="{{ url_for('web.category', name=cat.name) }}"
class="px-3 py-1.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-full hover:border-indigo-500 hover:text-indigo-600 transition-colors">
{{ cat.display_name }}
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+158
View File
@@ -0,0 +1,158 @@
{% extends "base.html" %}
{% block title %}Terms of Service - CmdForge{% endblock %}
{% block meta_description %}CmdForge terms of service. Read our terms and conditions for using the platform.{% endblock %}
{% block content %}
<div class="bg-white py-16">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Terms of Service</h1>
<p class="text-gray-500 mb-8">Last updated: January 2025</p>
<div class="prose prose-lg prose-indigo max-w-none">
<h2>1. Acceptance of Terms</h2>
<p>
By accessing or using CmdForge ("the Service"), you agree to be bound by these
Terms of Service ("Terms"). If you do not agree to these Terms, you may not use
the Service.
</p>
<h2>2. Description of Service</h2>
<p>
CmdForge is a platform for creating, publishing, and sharing AI-powered
command-line tools. The Service includes:
</p>
<ul>
<li>The CmdForge CLI application</li>
<li>The CmdForge Registry (tool hosting and discovery)</li>
<li>Documentation and tutorials</li>
<li>Community features</li>
</ul>
<h2>3. User Accounts</h2>
<p>
To publish tools or access certain features, you must create an account. You agree to:
</p>
<ul>
<li>Provide accurate and complete information</li>
<li>Maintain the security of your account credentials</li>
<li>Notify us immediately of any unauthorized access</li>
<li>Accept responsibility for all activities under your account</li>
</ul>
<h2>4. User Content</h2>
<h3>4.1 Your Content</h3>
<p>
You retain ownership of tools and content you publish ("User Content"). By publishing
to the Registry, you grant CmdForge a non-exclusive, worldwide license to host,
distribute, and display your User Content.
</p>
<h3>4.2 Content Standards</h3>
<p>You agree not to publish content that:</p>
<ul>
<li>Contains malware, viruses, or malicious code</li>
<li>Infringes on intellectual property rights</li>
<li>Is illegal, harmful, or promotes illegal activities</li>
<li>Harasses, threatens, or harms others</li>
<li>Contains spam or deceptive content</li>
<li>Violates the privacy of others</li>
</ul>
<h3>4.3 Content Removal</h3>
<p>
We reserve the right to remove any content that violates these Terms or that we
determine to be harmful to users or the community.
</p>
<h2>5. Acceptable Use</h2>
<p>You agree not to:</p>
<ul>
<li>Use the Service for any unlawful purpose</li>
<li>Attempt to gain unauthorized access to our systems</li>
<li>Interfere with or disrupt the Service</li>
<li>Scrape or collect data without permission</li>
<li>Impersonate others or misrepresent your affiliation</li>
<li>Use automated systems to access the Service excessively</li>
</ul>
<h2>6. API and CLI Usage</h2>
<p>
Access to the CmdForge API and CLI is subject to rate limits. Excessive use
that impacts service availability for others may result in temporary or permanent
restrictions.
</p>
<h2>7. Third-Party Services</h2>
<p>
CmdForge is designed to work with various AI providers and external services.
Your use of these third-party services is subject to their respective terms and
conditions. We are not responsible for third-party services.
</p>
<h2>8. Intellectual Property</h2>
<p>
The CmdForge software is open source and licensed under the MIT License.
The CmdForge name, logo, and branding are trademarks of CmdForge.
</p>
<h2>9. Disclaimer of Warranties</h2>
<p>
THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED.
WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE.
</p>
<p>
Tools published by third parties are not endorsed by CmdForge. You use third-party
tools at your own risk.
</p>
<h2>10. Limitation of Liability</h2>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, CMDFORGE SHALL NOT BE LIABLE FOR ANY
INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING FROM
YOUR USE OF THE SERVICE.
</p>
<h2>11. Indemnification</h2>
<p>
You agree to indemnify and hold harmless CmdForge and its contributors from
any claims, damages, or expenses arising from your use of the Service or
violation of these Terms.
</p>
<h2>12. Termination</h2>
<p>
We may terminate or suspend your access to the Service at any time, with or
without cause. Upon termination, your right to use the Service ceases immediately.
</p>
<p>
You may delete your account at any time through your account settings.
</p>
<h2>13. Changes to Terms</h2>
<p>
We may modify these Terms at any time. We will notify you of significant changes
by posting a notice on our website. Continued use of the Service after changes
constitutes acceptance of the new Terms.
</p>
<h2>14. Governing Law</h2>
<p>
These Terms are governed by the laws of the jurisdiction in which CmdForge
operates, without regard to conflict of law principles.
</p>
<h2>15. Contact</h2>
<p>
For questions about these Terms, please contact us at:
</p>
<ul>
<li>Email: [email protected]</li>
<li>GitHub: <a href="https://github.com/your-org/cmdforge/issues">Open an issue</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,284 @@
{% extends "base.html" %}
{% from "components/callouts.html" import warning, info %}
{% block title %}{{ tool.owner }}/{{ tool.name }} - CmdForge Registry{% endblock %}
{% block meta_description %}{{ tool.description or 'A CmdForge command-line tool' }}{% endblock %}
{% block og_title %}{{ tool.owner }}/{{ tool.name }}{% endblock %}
{% block og_description %}{{ tool.description or 'A CmdForge command-line tool' }}{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Breadcrumb -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<nav class="flex items-center text-sm text-gray-500" aria-label="Breadcrumb">
<a href="{{ url_for('web.tools') }}" class="hover:text-gray-700">Tools</a>
<svg class="w-4 h-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
<a href="{{ url_for('web.publisher', slug=tool.owner) }}" class="hover:text-gray-700">{{ tool.owner }}</a>
<svg class="w-4 h-4 mx-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
<span class="text-gray-900 font-medium">{{ tool.name }}</span>
</nav>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="lg:grid lg:grid-cols-3 lg:gap-8">
<!-- Main Content -->
<main class="lg:col-span-2">
<!-- Tool Header -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
<div class="flex items-start justify-between">
<div class="flex items-center">
<div class="w-16 h-16 bg-indigo-100 rounded-lg flex items-center justify-center">
<svg class="w-8 h-8 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<div class="ml-4">
<h1 class="text-2xl font-bold text-gray-900">{{ tool.name }}</h1>
<p class="text-gray-500">
by <a href="{{ url_for('web.publisher', slug=tool.owner) }}" class="text-indigo-600 hover:text-indigo-800">{{ tool.owner }}</a>
</p>
</div>
</div>
{% if tool.category %}
<span class="px-3 py-1 text-xs font-medium text-cyan-700 bg-cyan-100 rounded-full">
{{ tool.category }}
</span>
{% endif %}
</div>
{% if tool.description %}
<p class="mt-4 text-gray-600">{{ tool.description }}</p>
{% endif %}
{% if tool.tags %}
<div class="mt-4 flex flex-wrap gap-2">
{% for tag in tool.tags.split(',') %}
<span class="px-2 py-1 text-xs text-gray-600 bg-gray-100 rounded">{{ tag.strip() }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Deprecation Warning -->
{% if tool.deprecated %}
{{ warning(
title='This tool is deprecated',
content=tool.deprecated_message or 'This tool is no longer maintained.'
) }}
{% if tool.replacement %}
{{ info(
title='Recommended replacement',
content='Consider using <a href="' ~ url_for('web.tool_detail', owner=tool.replacement.split('/')[0], name=tool.replacement.split('/')[1]) ~ '" class="underline">' ~ tool.replacement ~ '</a> instead.'
) }}
{% endif %}
{% endif %}
<!-- README Content -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="prose prose-indigo max-w-none">
{% if tool.readme %}
{{ tool.readme_html|safe }}
{% else %}
<h2>About {{ tool.name }}</h2>
<p>{{ tool.description or 'No additional documentation available.' }}</p>
<h2>Installation</h2>
<pre><code>cmdforge install {{ tool.owner }}/{{ tool.name }}</code></pre>
<h2>Usage</h2>
<pre><code>{{ tool.name }} --help</code></pre>
{% endif %}
</div>
</div>
</main>
<!-- Sidebar -->
<aside class="mt-8 lg:mt-0">
<div class="sticky top-4 space-y-6">
<!-- Install Card -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Install</h3>
<div class="flex items-center bg-gray-100 rounded-lg p-3 group">
<span class="text-gray-400 mr-2 select-none">$</span>
<code id="install-cmd" class="flex-1 text-sm text-gray-800 font-mono overflow-x-auto">cmdforge install {{ tool.owner }}/{{ tool.name }}</code>
<button type="button"
onclick="copyInstall()"
class="ml-2 p-1.5 text-gray-400 hover:text-gray-600"
aria-label="Copy install command">
<svg id="copy-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg id="check-icon" class="w-4 h-4 hidden text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</button>
</div>
<button type="button"
onclick="submitReport('{{ tool.owner }}', '{{ tool.name }}')"
class="mt-4 w-full inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
Report Tool
</button>
</div>
<!-- Versions -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Versions</h3>
<ul class="space-y-2">
{% for version in versions %}
<li class="flex items-center justify-between">
<a href="{{ url_for('web.tool_version', owner=tool.owner, name=tool.name, version=version.version) }}"
class="text-sm {{ 'font-medium text-indigo-600' if version.version == tool.version else 'text-gray-600 hover:text-indigo-600' }}">
v{{ version.version }}
{% if version.version == tool.version %}
<span class="ml-1 text-xs text-green-600">(current)</span>
{% endif %}
</a>
<span class="text-xs text-gray-400">{{ version.published_at|timeago }}</span>
</li>
{% else %}
<li class="text-sm text-gray-500">v{{ tool.version }}</li>
{% endfor %}
</ul>
</div>
<!-- Stats -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Stats</h3>
<dl class="space-y-3">
<div class="flex items-center justify-between">
<dt class="text-sm text-gray-500">Downloads</dt>
<dd class="text-sm font-medium text-gray-900">{{ tool.downloads|format_number }}</dd>
</div>
<div class="flex items-center justify-between">
<dt class="text-sm text-gray-500">Published</dt>
<dd class="text-sm font-medium text-gray-900">{{ tool.published_at|timeago }}</dd>
</div>
{% if tool.category %}
<div class="flex items-center justify-between">
<dt class="text-sm text-gray-500">Category</dt>
<dd>
<a href="{{ url_for('web.category', name=tool.category) }}"
class="text-sm font-medium text-indigo-600 hover:text-indigo-800">
{{ tool.category }}
</a>
</dd>
</div>
{% endif %}
</dl>
</div>
<!-- Publisher -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Publisher</h3>
<a href="{{ url_for('web.publisher', slug=tool.owner) }}"
class="flex items-center group">
<div class="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
<span class="text-gray-600 font-medium">{{ tool.owner[0]|upper }}</span>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-gray-900 group-hover:text-indigo-600">
{{ publisher.display_name if publisher else tool.owner }}
</p>
<p class="text-xs text-gray-500">@{{ tool.owner }}</p>
</div>
{% if publisher and publisher.verified %}
<svg class="ml-auto w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
{% endif %}
</a>
</div>
<!-- Report Tool -->
<div class="text-center">
<button type="button"
onclick="submitReport('{{ tool.owner }}', '{{ tool.name }}')"
class="text-sm text-gray-500 hover:text-red-600 transition-colors">
Report this tool
</button>
</div>
</div>
</aside>
</div>
</div>
</div>
<!-- Report Modal -->
<div id="report-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
<div class="min-h-screen px-4 text-center">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" onclick="closeReportModal()"></div>
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
<form action="/api/v1/reports" method="POST" id="report-form">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900">Report Tool</h3>
<p class="mt-2 text-sm text-gray-600">
Help us maintain a safe registry by reporting tools that violate our guidelines.
</p>
<input type="hidden" name="tool_owner" id="report-owner" value="{{ tool.owner }}">
<input type="hidden" name="tool_name" id="report-name" value="{{ tool.name }}">
<div class="mt-6 space-y-4">
<div>
<label for="reason" class="block text-sm font-medium text-gray-700 mb-1">Reason</label>
<select name="reason" id="reason" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
<option value="">Select a reason...</option>
<option value="spam">Spam</option>
<option value="malicious">Malicious</option>
<option value="duplicate">Duplicate</option>
<option value="inappropriate">Inappropriate</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description (optional)</label>
<textarea name="description" id="description" rows="3"
placeholder="Provide additional context"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 resize-none"></textarea>
</div>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
<button type="button"
onclick="closeReportModal()"
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
Cancel
</button>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
Submit Report
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function copyInstall() {
const cmd = 'cmdforge install {{ tool.owner }}/{{ tool.name }}';
navigator.clipboard.writeText(cmd).then(() => {
const copyIcon = document.getElementById('copy-icon');
const checkIcon = document.getElementById('check-icon');
copyIcon.classList.add('hidden');
checkIcon.classList.remove('hidden');
setTimeout(() => {
copyIcon.classList.remove('hidden');
checkIcon.classList.add('hidden');
}, 2000);
});
}
</script>
{% endblock %}
+232
View File
@@ -0,0 +1,232 @@
{% extends "base.html" %}
{% from "components/tool_card.html" import tool_card %}
{% block title %}Browse Tools - CmdForge Registry{% endblock %}
{% block meta_description %}Discover and install community-built AI tools. Browse by category, search by name, or explore the most popular tools.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Page Header -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-3xl font-bold text-gray-900">Browse Tools</h1>
<p class="mt-2 text-gray-600">
Discover community-built AI tools for your command line.
</p>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="lg:grid lg:grid-cols-4 lg:gap-8">
<!-- Sidebar Filters -->
<aside class="hidden lg:block">
<div class="sticky top-4 space-y-6">
<!-- Search -->
<div>
<label for="search" class="block text-sm font-medium text-gray-700 mb-2">
Search
</label>
<form action="{{ url_for('web.search') }}" method="GET">
<div class="relative">
<input type="text"
name="q"
id="search"
value="{{ query or '' }}"
placeholder="Search tools..."
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<button type="submit" class="absolute left-3 top-2.5" aria-label="Search">
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</button>
</div>
</form>
</div>
<!-- Categories -->
<div>
<h3 class="text-sm font-medium text-gray-700 mb-3">Categories</h3>
<ul class="space-y-2">
<li>
<a href="{{ url_for('web.tools') }}"
class="flex items-center justify-between px-3 py-2 rounded-lg {{ 'bg-indigo-50 text-indigo-600 font-medium' if not current_category else 'text-gray-600 hover:bg-gray-100' }}">
<span>All Tools</span>
<span class="text-sm text-gray-600">{{ total_count }}</span>
</a>
</li>
{% for cat in categories %}
<li>
<a href="{{ url_for('web.category', name=cat.name) }}"
class="flex items-center justify-between px-3 py-2 rounded-lg {{ 'bg-indigo-50 text-indigo-600 font-medium' if current_category == cat.name else 'text-gray-600 hover:bg-gray-100' }}">
<span>{{ cat.display_name }}</span>
<span class="text-sm text-gray-600">{{ cat.count }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
<!-- Sort -->
<div>
<label for="sort-select" class="block text-sm font-medium text-gray-700 mb-3">Sort By</label>
<select id="sort-select"
onchange="updateSort(this.value)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 bg-white">
<option value="downloads" {{ 'selected' if sort == 'downloads' }}>Most Downloads</option>
<option value="published_at" {{ 'selected' if sort == 'published_at' }}>Recently Published</option>
<option value="name" {{ 'selected' if sort == 'name' }}>Name (A-Z)</option>
</select>
</div>
</div>
</aside>
<!-- Mobile Filter Toggle -->
<div class="lg:hidden mb-6">
<button type="button"
onclick="toggleMobileFilters()"
class="w-full flex items-center justify-between px-4 py-3 bg-white border border-gray-300 rounded-lg">
<span class="font-medium text-gray-700">Filters & Sort</span>
<svg id="filter-chevron" class="w-5 h-5 text-gray-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<!-- Mobile Filters Panel -->
<div id="mobile-filters" class="hidden mt-4 p-4 bg-white border border-gray-200 rounded-lg space-y-4">
<!-- Mobile Search -->
<form action="{{ url_for('web.search') }}" method="GET" class="flex gap-2">
<input type="text"
name="q"
value="{{ query or '' }}"
placeholder="Search tools..."
aria-label="Search tools"
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700" aria-label="Search">
Search
</button>
</form>
<!-- Mobile Categories -->
<div>
<label for="mobile-category-select" class="block text-sm font-medium text-gray-700 mb-2">Category</label>
<select id="mobile-category-select"
onchange="window.location.href=this.value"
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white">
<option value="{{ url_for('web.tools') }}" {{ 'selected' if not current_category }}>All Tools</option>
{% for cat in categories %}
<option value="{{ url_for('web.category', name=cat.name) }}" {{ 'selected' if current_category == cat.name }}>
{{ cat.display_name }}
</option>
{% endfor %}
</select>
</div>
<!-- Mobile Sort -->
<div>
<label for="mobile-sort-select" class="block text-sm font-medium text-gray-700 mb-2">Sort By</label>
<select id="mobile-sort-select"
onchange="updateSort(this.value)"
class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white">
<option value="downloads" {{ 'selected' if sort == 'downloads' }}>Most Downloads</option>
<option value="published_at" {{ 'selected' if sort == 'published_at' }}>Recently Published</option>
<option value="name" {{ 'selected' if sort == 'name' }}>Name (A-Z)</option>
</select>
</div>
</div>
</div>
<!-- Tools Grid -->
<main class="lg:col-span-3">
<!-- Results Header -->
<div class="flex items-center justify-between mb-6">
<p class="text-sm text-gray-600">
{% if query %}
{{ tools|length }} results for "{{ query }}"
{% elif current_category %}
{{ tools|length }} tools in {{ current_category }}
{% else %}
{{ tools|length }} tools available
{% endif %}
</p>
</div>
{% if tools %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{% for tool in tools %}
{{ tool_card(
owner=tool.owner,
name=tool.name,
description=tool.description,
category=tool.category,
downloads=tool.downloads,
version=tool.version
) }}
{% endfor %}
</div>
<!-- Pagination -->
{% if pagination %}
<nav class="mt-12 flex items-center justify-center" aria-label="Pagination">
<div class="flex items-center gap-2">
{% if pagination.has_prev %}
<a href="{{ url_for('web.tools', page=pagination.prev_num, sort=sort, category=current_category) }}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Previous
</a>
{% endif %}
<span class="px-4 py-2 text-sm text-gray-700">
Page {{ pagination.page }} of {{ pagination.pages }}
</span>
{% if pagination.has_next %}
<a href="{{ url_for('web.tools', page=pagination.next_num, sort=sort, category=current_category) }}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
Next
</a>
{% endif %}
</div>
</nav>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="text-center py-16">
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No tools found</h3>
<p class="mt-2 text-gray-600">
{% if query %}
No tools match your search. Try different keywords.
{% else %}
Be the first to publish a tool in this category!
{% endif %}
</p>
<a href="{{ url_for('web.docs', path='publishing') }}"
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
Learn to Publish
</a>
</div>
{% endif %}
</main>
</div>
</div>
</div>
<script>
function toggleMobileFilters() {
const filters = document.getElementById('mobile-filters');
const chevron = document.getElementById('filter-chevron');
filters.classList.toggle('hidden');
chevron.classList.toggle('rotate-180');
}
function updateSort(value) {
const url = new URL(window.location.href);
url.searchParams.set('sort', value);
window.location.href = url.toString();
}
</script>
{% endblock %}
@@ -0,0 +1,195 @@
{% extends "base.html" %}
{% from "components/tutorial_card.html" import tutorial_card %}
{% block title %}Tutorials - CmdForge{% endblock %}
{% block meta_description %}Learn how to use CmdForge with step-by-step tutorials. From basic setup to advanced workflows.{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen">
<!-- Hero -->
<div class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
<h1 class="text-3xl font-bold text-gray-900">Tutorials</h1>
<p class="mt-4 text-lg text-gray-600">
Learn CmdForge from the ground up with step-by-step guides.
</p>
</div>
</div>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Getting Started Section -->
<section class="mb-16">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Getting Started</h2>
<p class="text-gray-600 mb-8">
New to CmdForge? Start here with the fundamentals.
</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{{ tutorial_card(
title="Installation & Setup",
description="Install CmdForge and configure your first AI provider.",
href=url_for('web.tutorials_path', path='installation'),
step_number=1
) }}
{{ tutorial_card(
title="Your First Tool",
description="Create a simple AI-powered command in under 5 minutes.",
href=url_for('web.tutorials_path', path='first-tool'),
step_number=2
) }}
{{ tutorial_card(
title="Understanding YAML Config",
description="Learn the structure of CmdForge configuration files.",
href=url_for('web.tutorials_path', path='yaml-config'),
step_number=3
) }}
</div>
</section>
<!-- Core Concepts Section -->
<section class="mb-16">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Core Concepts</h2>
<p class="text-gray-600 mb-8">
Understand the key concepts that power CmdForge.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{% for tutorial in core_tutorials %}
<a href="{{ url_for('web.tutorials_path', path=tutorial.slug) }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">{{ tutorial.title }}</h3>
<p class="text-gray-600 text-sm">{{ tutorial.description }}</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
{% else %}
<a href="{{ url_for('web.tutorials_path', path='providers') }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Working with Providers</h3>
<p class="text-gray-600 text-sm">Configure and use different AI providers with your tools.</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
<a href="{{ url_for('web.tutorials_path', path='arguments') }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Custom Arguments</h3>
<p class="text-gray-600 text-sm">Add flags and options to make your tools flexible.</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
<a href="{{ url_for('web.tutorials_path', path='multi-step') }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Multi-Step Workflows</h3>
<p class="text-gray-600 text-sm">Chain prompts and code steps together.</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
<a href="{{ url_for('web.tutorials_path', path='code-steps') }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Code Steps</h3>
<p class="text-gray-600 text-sm">Add Python code processing between AI calls.</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
{% endfor %}
</div>
</section>
<!-- Advanced Section -->
<section class="mb-16">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Advanced Topics</h2>
<p class="text-gray-600 mb-8">
Take your tools to the next level with advanced techniques.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<a href="{{ url_for('web.tutorials_path', path='publishing') }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Publishing Tools</h3>
<p class="text-gray-600 text-sm">Share your tools with the community via the registry.</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
<a href="{{ url_for('web.tutorials_path', path='advanced-workflows') }}"
class="block bg-white rounded-lg border border-gray-200 p-6 hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900 mb-2">Advanced Workflows</h3>
<p class="text-gray-600 text-sm">Complex multi-provider and branching workflows.</p>
<span class="mt-4 inline-flex items-center text-sm text-indigo-600">
Read tutorial
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
</span>
</a>
</div>
</section>
<!-- Video Tutorials -->
{% if video_tutorials %}
<section>
<h2 class="text-2xl font-bold text-gray-900 mb-6">Video Tutorials</h2>
<p class="text-gray-600 mb-8">
Prefer watching? Check out our video guides.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{% for video in video_tutorials %}
<a href="{{ video.url }}"
target="_blank"
rel="noopener noreferrer"
class="block bg-white rounded-lg border border-gray-200 overflow-hidden hover:shadow-lg transition-shadow">
<div class="relative">
<img src="{{ video.thumbnail }}"
alt="{{ video.title }}"
class="w-full h-48 object-cover">
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-16 h-16 bg-black bg-opacity-60 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-white ml-1" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z"/>
</svg>
</div>
</div>
</div>
<div class="p-4">
<h3 class="font-semibold text-gray-900">{{ video.title }}</h3>
<p class="text-sm text-gray-500 mt-1">{{ video.duration }}</p>
</div>
</a>
{% endfor %}
</div>
</section>
{% endif %}
</div>
</div>
{% endblock %}