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
+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 %}