956797f75b
Add 'ci' category with two service generators (gitlab, jenkins), following project conventions (quoted heredocs, PKG_INSTALL, bash_header, warnings + post_steps). Jenkins WAR URLs pinned in a class dict to avoid 404s. app.py + generators/__init__.py updated to register.
160 lines
5.4 KiB
Python
160 lines
5.4 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
|
|
from generators.ci 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)
|