Initial commit: 50 shell-script generators with web UI
- 8 categories / 50 generators covering middleware, databases, runtimes, systemd services, system tools, network, monitoring, security - VNC supports XFCE / GNOME / KDE Plasma / MATE / LXQt desktops - Node.js versions 18-26 (incl. current LTS 24 Krypton and current 26) - Live preview, copy / download / multi-script bundle in web UI - Distro-aware (Ubuntu/Debian/CentOS/RHEL/Rocky/Alma/Fedora) - All 50 generators pass bash -n syntax check - Zero pip dependencies (Flask stdlib only)
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Shell Generator - One-click Linux shell script generator
|
||||
Single-file Flask app + dedicated generator modules.
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import zipfile
|
||||
import datetime
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from flask import (Flask, render_template, request, jsonify, Response,
|
||||
send_file, abort, redirect, url_for, session, flash)
|
||||
|
||||
# Import all generator modules so they self-register via @register
|
||||
from generators import ( # noqa: F401
|
||||
list_generators, get_generator, get_categories,
|
||||
category_labels, category_icons,
|
||||
)
|
||||
from generators.middleware import * # noqa: F401 (registers)
|
||||
from generators.runtimes import * # noqa: F401
|
||||
from generators.systemd_svc import * # noqa: F401
|
||||
from generators.system import * # noqa: F401
|
||||
from generators.network import * # noqa: F401
|
||||
from generators.databases import * # noqa: F401
|
||||
from generators.monitoring import * # noqa: F401
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
app = Flask(__name__, instance_path=str(BASE_DIR / "instance"))
|
||||
app.config["SECRET_KEY"] = os.environ.get("SHELLGEN_SECRET", "shell-gen-default-secret-change-me")
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
|
||||
|
||||
# ----------------------------- Routes -----------------------------
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template(
|
||||
"index.html",
|
||||
categories=get_categories(),
|
||||
category_labels=category_labels,
|
||||
category_icons=category_icons,
|
||||
total=len(list_generators()),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/gen/<generator_id>")
|
||||
def generator_page(generator_id):
|
||||
gen = get_generator(generator_id)
|
||||
if gen is None:
|
||||
abort(404, f"Unknown generator: {generator_id}")
|
||||
return render_template(
|
||||
"generator.html",
|
||||
gen=gen,
|
||||
category_labels=category_labels,
|
||||
category_icons=category_icons,
|
||||
all_cats=get_categories(),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/preview", methods=["POST"])
|
||||
def api_preview():
|
||||
"""Generate script preview without running anything."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
gen_id = data.get("generator")
|
||||
params = data.get("params", {})
|
||||
gen = get_generator(gen_id) if gen_id else None
|
||||
if gen is None:
|
||||
return jsonify({"ok": False, "error": f"Unknown generator: {gen_id}"}), 400
|
||||
try:
|
||||
script = gen.render(params)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"{type(e).__name__}: {e}"}), 400
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"script": script,
|
||||
"title": gen.title,
|
||||
"description": gen.description,
|
||||
"warnings": gen.warnings,
|
||||
"post_steps": gen.post_steps,
|
||||
"verify_steps": gen.verify_steps,
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/download", methods=["POST"])
|
||||
def api_download():
|
||||
data = request.get_json(silent=True) or {}
|
||||
gen_id = data.get("generator")
|
||||
params = data.get("params", {})
|
||||
gen = get_generator(gen_id) if gen_id else None
|
||||
if gen is None:
|
||||
return jsonify({"ok": False, "error": f"Unknown generator: {gen_id}"}), 400
|
||||
try:
|
||||
script = gen.render(params)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": f"{type(e).__name__}: {e}"}), 400
|
||||
fname = f"{gen_id}.sh"
|
||||
headers = {"Content-Disposition": f'attachment; filename="{fname}"'}
|
||||
return Response(script, mimetype="text/x-shellscript", headers=headers)
|
||||
|
||||
|
||||
@app.route("/api/bundle", methods=["POST"])
|
||||
def api_bundle():
|
||||
"""Bundle multiple generators into a single zip of .sh files."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
items = data.get("items", [])
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for entry in items:
|
||||
gid = entry.get("generator")
|
||||
params = entry.get("params", {})
|
||||
gen = get_generator(gid)
|
||||
if gen is None:
|
||||
continue
|
||||
try:
|
||||
script = gen.render(params)
|
||||
except Exception as e:
|
||||
script = f"#!/usr/bin/env bash\necho 'ERROR generating {gid}: {e}'\nexit 1\n"
|
||||
z.writestr(f"{gid}.sh", script)
|
||||
# Master installer
|
||||
master = ["#!/usr/bin/env bash", "set -e", "# Auto-generated by shell-gen"]
|
||||
for entry in items:
|
||||
gid = entry.get("generator")
|
||||
if get_generator(gid) is None:
|
||||
continue
|
||||
master.append(f'echo "===> {gid}"')
|
||||
master.append(f"bash \"$(dirname \"$0\")/{gid}.sh\"")
|
||||
z.writestr("install-all.sh", "\n".join(master) + "\n")
|
||||
buf.seek(0)
|
||||
return send_file(buf, mimetype="application/zip", as_attachment=True,
|
||||
download_name=f"shell-gen-bundle-{datetime.datetime.now():%Y%m%d-%H%M%S}.zip")
|
||||
|
||||
|
||||
@app.route("/api/categories")
|
||||
def api_categories():
|
||||
out = []
|
||||
for cat in get_categories():
|
||||
gens = [g for g in list_generators().values() if g.category == cat]
|
||||
out.append({
|
||||
"id": cat,
|
||||
"label": category_labels.get(cat, cat),
|
||||
"icon": category_icons.get(cat, "📦"),
|
||||
"items": [g.summary() for g in gens],
|
||||
})
|
||||
return jsonify({"ok": True, "categories": out})
|
||||
|
||||
|
||||
@app.route("/healthz")
|
||||
def healthz():
|
||||
return jsonify({"ok": True, "generators": len(list_generators())})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.makedirs(app.instance_path, exist_ok=True)
|
||||
with app.app_context():
|
||||
# Touch a db-less startup log
|
||||
pass
|
||||
port = int(os.environ.get("PORT", 5099))
|
||||
app.run(host="0.0.0.0", port=port, debug=False)
|
||||
Reference in New Issue
Block a user