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