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,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sync registry tools into the database.
|
||||
|
||||
Usage: python scripts/sync_to_db.py /path/to/registry/repo
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
import yaml
|
||||
|
||||
# Allow running from repo root
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from smarttools.registry.db import connect_db, query_one
|
||||
from smarttools.registry.sync import ensure_publisher, normalize_tags
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> Dict[str, Any]:
|
||||
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
def sync_repo(repo_root: Path) -> int:
|
||||
tools_root = repo_root / "tools"
|
||||
if not tools_root.exists():
|
||||
print(f"Missing tools directory at {tools_root}")
|
||||
return 1
|
||||
|
||||
conn = connect_db()
|
||||
synced = 0
|
||||
skipped = 0
|
||||
|
||||
try:
|
||||
for config_path in tools_root.glob("*/*/config.yaml"):
|
||||
owner = config_path.parent.parent.name
|
||||
name = config_path.parent.name
|
||||
config_text = config_path.read_text(encoding="utf-8")
|
||||
data = load_yaml(config_path)
|
||||
version = (data.get("version") or "").strip()
|
||||
if not version:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
existing = query_one(
|
||||
conn,
|
||||
"SELECT id FROM tools WHERE owner = ? AND name = ? AND version = ?",
|
||||
[owner, name, version],
|
||||
)
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
readme_path = config_path.parent / "README.md"
|
||||
readme_text = readme_path.read_text(encoding="utf-8") if readme_path.exists() else ""
|
||||
|
||||
publisher_id = ensure_publisher(conn, owner)
|
||||
tags = normalize_tags(data.get("tags"))
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO tools (
|
||||
owner, name, version, description, category, tags, config_yaml, readme,
|
||||
publisher_id, deprecated, deprecated_message, replacement, downloads, published_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
owner,
|
||||
name,
|
||||
version,
|
||||
data.get("description"),
|
||||
data.get("category"),
|
||||
tags,
|
||||
config_text,
|
||||
readme_text,
|
||||
publisher_id,
|
||||
int(bool(data.get("deprecated"))),
|
||||
data.get("deprecated_message"),
|
||||
data.get("replacement"),
|
||||
int((data.get("registry") or {}).get("downloads", 0) or 0),
|
||||
(data.get("registry") or {}).get("published_at"),
|
||||
],
|
||||
)
|
||||
synced += 1
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(f"Synced: {synced}")
|
||||
print(f"Skipped (existing/invalid): {skipped}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python scripts/sync_to_db.py /path/to/registry/repo")
|
||||
return 1
|
||||
|
||||
repo_root = Path(sys.argv[1])
|
||||
return sync_repo(repo_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a registry tool submission.
|
||||
|
||||
Usage: python scripts/validate_tool.py path/to/tool
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
import yaml
|
||||
|
||||
TOOL_NAME_RE = re.compile(r"^[A-Za-z0-9-]{1,64}$")
|
||||
SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+.+)?$")
|
||||
|
||||
REQUIRED_README_SECTIONS = ["## Usage", "## Examples"]
|
||||
|
||||
|
||||
def find_repo_root(start: Path) -> Path | None:
|
||||
current = start.resolve()
|
||||
while current != current.parent:
|
||||
if (current / "categories" / "categories.yaml").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
if (current / "categories" / "categories.yaml").exists():
|
||||
return current
|
||||
return None
|
||||
|
||||
|
||||
def load_categories(repo_root: Path) -> List[str]:
|
||||
categories_path = repo_root / "categories" / "categories.yaml"
|
||||
data = yaml.safe_load(categories_path.read_text(encoding="utf-8")) or {}
|
||||
categories = data.get("categories", [])
|
||||
return [c.get("name") for c in categories if c.get("name")]
|
||||
|
||||
|
||||
def validate_tool(tool_path: Path) -> List[str]:
|
||||
errors: List[str] = []
|
||||
if tool_path.is_dir():
|
||||
config_path = tool_path / "config.yaml"
|
||||
readme_path = tool_path / "README.md"
|
||||
else:
|
||||
config_path = tool_path
|
||||
readme_path = tool_path.parent / "README.md"
|
||||
|
||||
if not config_path.exists():
|
||||
return [f"Missing config.yaml at {config_path}"]
|
||||
|
||||
try:
|
||||
config_text = config_path.read_text(encoding="utf-8")
|
||||
data = yaml.safe_load(config_text) or {}
|
||||
except Exception as exc:
|
||||
return [f"Invalid YAML in config.yaml: {exc}"]
|
||||
|
||||
name = (data.get("name") or "").strip()
|
||||
version = (data.get("version") or "").strip()
|
||||
description = (data.get("description") or "").strip()
|
||||
category = (data.get("category") or "").strip()
|
||||
|
||||
if not name:
|
||||
errors.append("Missing required field: name")
|
||||
elif not TOOL_NAME_RE.match(name):
|
||||
errors.append("Tool name must match ^[A-Za-z0-9-]{1,64}$")
|
||||
|
||||
if not version:
|
||||
errors.append("Missing required field: version")
|
||||
elif not SEMVER_RE.match(version):
|
||||
errors.append("Version must be valid semver (MAJOR.MINOR.PATCH)")
|
||||
|
||||
if not description:
|
||||
errors.append("Missing required field: description")
|
||||
|
||||
repo_root = find_repo_root(tool_path)
|
||||
if repo_root:
|
||||
categories = load_categories(repo_root)
|
||||
if category and category not in categories:
|
||||
errors.append(f"Unknown category '{category}' (not in categories.yaml)")
|
||||
else:
|
||||
if category:
|
||||
errors.append("Cannot validate category (categories.yaml not found)")
|
||||
|
||||
if not readme_path.exists():
|
||||
errors.append(f"Missing README.md at {readme_path}")
|
||||
else:
|
||||
readme_text = readme_path.read_text(encoding="utf-8")
|
||||
for section in REQUIRED_README_SECTIONS:
|
||||
if section not in readme_text:
|
||||
errors.append(f"README.md missing section: {section}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python scripts/validate_tool.py path/to/tool")
|
||||
return 1
|
||||
|
||||
tool_path = Path(sys.argv[1])
|
||||
errors = validate_tool(tool_path)
|
||||
if errors:
|
||||
print("Validation failed:")
|
||||
for err in errors:
|
||||
print(f"- {err}")
|
||||
return 1
|
||||
|
||||
print("Validation passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user