Files
shell-gen/app.py
T
cnbug 319f683a16 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)
2026-08-04 00:36:35 +08:00

159 lines
5.3 KiB
Python

"""
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)