diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..3089bbc --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,35 @@ +"""Gunicorn configuration for CmdForge web service.""" + +import os + +# Server socket +bind = "0.0.0.0:" + os.environ.get("PORT", "5050") +backlog = 2048 + +# Worker processes +workers = 2 # Conservative for shared server +worker_class = "sync" +worker_connections = 1000 +timeout = 120 +keepalive = 2 + +# Process naming +proc_name = "cmdforge-web" + +# Logging +accesslog = "-" # stdout +errorlog = "-" # stderr +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + +# Server mechanics +daemon = False +pidfile = None +umask = 0 +user = None +group = None +tmp_upload_dir = None + +# SSL (handled by Cloudflare, not needed here) +keyfile = None +certfile = None diff --git a/pyproject.toml b/pyproject.toml index 1cbf68c..fa090e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,12 +47,14 @@ registry = [ "Flask>=2.3", "argon2-cffi>=21.0", "sentry-sdk[flask]>=1.0", + "gunicorn>=21.0", ] all = [ "urwid>=2.1.0", "Flask>=2.3", "argon2-cffi>=21.0", "sentry-sdk[flask]>=1.0", + "gunicorn>=21.0", ] [project.scripts] diff --git a/src/cmdforge/registry/app.py b/src/cmdforge/registry/app.py index c38c750..0b358bb 100644 --- a/src/cmdforge/registry/app.py +++ b/src/cmdforge/registry/app.py @@ -574,6 +574,21 @@ def create_app() -> Flask: if not row: return error_response("TOOL_NOT_FOUND", f"Tool '{owner}/{name}' does not exist", 404) + # Parse source attribution + source_obj = None + if row["source_json"]: + try: + source_obj = json.loads(row["source_json"]) + except (json.JSONDecodeError, TypeError): + pass + # Fall back to legacy fields if no source_json + if not source_obj and (row["source"] or row["source_url"]): + source_obj = { + "type": "imported", + "original_tool": row["source"], + "url": row["source_url"], + } + payload = { "owner": row["owner"], "name": row["name"], @@ -588,6 +603,7 @@ def create_app() -> Flask: "replacement": row["replacement"], "config": row["config_yaml"], "readme": row["readme"], + "source": source_obj, } response = jsonify({"data": payload}) response.headers["Cache-Control"] = "max-age=60" @@ -1100,9 +1116,25 @@ def create_app() -> Flask: description = (data.get("description") or "").strip() category = (data.get("category") or "").strip() or None tags = data.get("tags") or [] - source = (data.get("source") or "").strip() or None + + # Handle source attribution - can be a dict (full ToolSource) or string (legacy) + source_data = data.get("source") + source_json = None + source = None source_url = (data.get("source_url") or "").strip() or None + if isinstance(source_data, dict): + # Full source object from tool YAML + source_json = json.dumps(source_data) + # Keep legacy fields for backward compat + source = source_data.get("original_tool") or source_data.get("author") + source_url = source_data.get("url") or source_url + elif isinstance(source_data, str) and source_data.strip(): + # Legacy string format + source = source_data.strip() + # Create a minimal source_json for consistency + source_json = json.dumps({"type": "imported", "original_tool": source, "url": source_url}) + if not name or not TOOL_NAME_RE.match(name) or len(name) > MAX_TOOL_NAME_LEN: return error_response("VALIDATION_ERROR", "Invalid tool name") if not version or Semver.parse(version) is None: @@ -1218,8 +1250,8 @@ def create_app() -> Flask: INSERT INTO tools ( owner, name, version, description, category, tags, config_yaml, readme, publisher_id, deprecated, deprecated_message, replacement, downloads, - scrutiny_status, scrutiny_report, source, source_url, published_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + scrutiny_status, scrutiny_report, source, source_url, source_json, published_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, [ owner, @@ -1239,6 +1271,7 @@ def create_app() -> Flask: scrutiny_json, source, source_url, + source_json, datetime.utcnow().isoformat(), ], ) diff --git a/src/cmdforge/registry/db.py b/src/cmdforge/registry/db.py index 8ce8bad..f4d1b0e 100644 --- a/src/cmdforge/registry/db.py +++ b/src/cmdforge/registry/db.py @@ -53,6 +53,7 @@ CREATE TABLE IF NOT EXISTS tools ( scrutiny_report TEXT, source TEXT, source_url TEXT, + source_json TEXT, published_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(owner, name, version) ); diff --git a/src/cmdforge/web/templates/pages/tool_detail.html b/src/cmdforge/web/templates/pages/tool_detail.html index 8ffc37a..37cd669 100644 --- a/src/cmdforge/web/templates/pages/tool_detail.html +++ b/src/cmdforge/web/templates/pages/tool_detail.html @@ -68,19 +68,34 @@ {% if tool.source %}
-
- +
+ - - Based on - {% if tool.source_url %} - {{ tool.source }} +
+ {% if tool.source.type == 'forked' %} + Forked from + {% elif tool.source.type == 'imported' %} + Based on {% else %} - {{ tool.source }} + Source: {% endif %} - pattern - + {% if tool.source.original_tool %} + {% if tool.source.url %} + {{ tool.source.original_tool }} + {% else %} + {{ tool.source.original_tool }} + {% endif %} + {% elif tool.source.url %} + {{ tool.source.url }} + {% endif %} + {% if tool.source.author %} + Author: {{ tool.source.author }} + {% endif %} + {% if tool.source.license %} + License: {{ tool.source.license }} + {% endif %} +
{% endif %}