Add SmartTools Registry and Web UI (Phases 1-7)
Registry Features (Phases 1-6): - Tool manifest format and validation - Registry client for API communication - Resolver integration for tool installation - CLI commands: install, search, browse, publish, auth - TUI browse interface with urwid - Smart features: auto-categorization, similarity suggestions Web UI (Phase 7): - Flask blueprint with server-side sessions - Authentication routes (login, register, logout) - Public pages: landing, tools browse, tool detail, search - Dashboard: overview, tools management, API tokens, settings - Template components: cards, forms, callouts, code blocks - Tailwind CSS build pipeline - SEO infrastructure (sitemap, robots.txt) - Cookie consent and privacy compliance Infrastructure: - SQLite database with FTS5 search - Rate limiting for API endpoints - Git-based tool submission workflow - Session management with auto-cleanup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/callouts.html" import warning, info %}
|
||||
|
||||
{% block title %}{{ tool.owner }}/{{ tool.name }} - SmartTools Registry{% endblock %}
|
||||
|
||||
{% block meta_description %}{{ tool.description or 'A SmartTools command-line tool' }}{% endblock %}
|
||||
|
||||
{% block og_title %}{{ tool.owner }}/{{ tool.name }}{% endblock %}
|
||||
{% block og_description %}{{ tool.description or 'A SmartTools 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>smarttools 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">smarttools 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>
|
||||
</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="openReportModal()"
|
||||
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_id" value="{{ tool.id }}">
|
||||
|
||||
<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="malicious">Malicious code</option>
|
||||
<option value="spam">Spam or advertising</option>
|
||||
<option value="copyright">Copyright violation</option>
|
||||
<option value="inappropriate">Inappropriate content</option>
|
||||
<option value="broken">Broken or non-functional</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="details" class="block text-sm font-medium text-gray-700 mb-1">Details (optional)</label>
|
||||
<textarea name="details" id="details" 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 = 'smarttools 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);
|
||||
});
|
||||
}
|
||||
|
||||
function openReportModal() {
|
||||
document.getElementById('report-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeReportModal() {
|
||||
document.getElementById('report-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Handle report form submission
|
||||
document.getElementById('report-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/reports', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
closeReportModal();
|
||||
alert('Thank you for your report. We will review it shortly.');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to submit report. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to submit report. Please try again.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user