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,33 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{% block title %}Shell-Gen · Linux 部署脚本生成器{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<span class="logo">⚙️</span>
|
||||
<span class="title">shell-gen</span>
|
||||
<span class="subtitle">Linux 一键部署脚本生成器</span>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<a href="{{ url_for('index') }}">首页</a>
|
||||
<span class="sep">·</span>
|
||||
<span class="count">{{ total }} 个生成器</span>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% block body %}{% endblock %}
|
||||
|
||||
<footer class="footer">
|
||||
<span>Generated by shell-gen · {{ total }} generators</span>
|
||||
<span>·</span>
|
||||
<a href="/healthz">/healthz</a>
|
||||
<span>·</span>
|
||||
<span>Compatible with Ubuntu / Debian / CentOS / RHEL / Rocky / Alma</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,253 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ gen.title }} · Shell-Gen{% endblock %}
|
||||
{% block body %}
|
||||
<main class="container">
|
||||
<nav class="breadcrumb">
|
||||
<a href="{{ url_for('index') }}">← 返回首页</a>
|
||||
<span class="cat-icon">{{ category_icons.get(gen.category, '📦') }}</span>
|
||||
<span class="cat-label">{{ category_labels.get(gen.category, gen.category) }}</span>
|
||||
</nav>
|
||||
|
||||
<header class="gen-header">
|
||||
<div class="gen-icon">{{ gen.icon }}</div>
|
||||
<div class="gen-meta">
|
||||
<h1>{{ gen.title }}</h1>
|
||||
<p>{{ gen.description }}</p>
|
||||
<div class="gen-tags">
|
||||
{% for t in gen.tags %}<span class="tag">#{{ t }}</span>{% endfor %}
|
||||
{% for o in gen.os_support %}<span class="os">{{ o }}</span>{% endfor %}
|
||||
{% if gen.dangerous %}<span class="dangerous">⚠️ 危险操作</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if gen.warnings %}
|
||||
<div class="warnings">
|
||||
<h3>⚠️ 注意事项</h3>
|
||||
<ul>
|
||||
{% for w in gen.warnings %}<li>{{ w }}</li>{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid-2col">
|
||||
<section class="form-card">
|
||||
<h2>📝 参数</h2>
|
||||
<form id="gen-form" data-gen="{{ gen.id }}">
|
||||
{% for f in gen.fields %}
|
||||
<div class="field" data-group="{{ f.group or '' }}">
|
||||
<label for="f-{{ f.name }}">
|
||||
{{ f.label }}
|
||||
{% if f.required %}<span class="req">*</span>{% endif %}
|
||||
</label>
|
||||
{% if f.type == 'select' %}
|
||||
<select id="f-{{ f.name }}" name="{{ f.name }}" {% if f.required %}required{% endif %}>
|
||||
{% for opt in f.options %}<option value="{{ opt }}" {% if opt == f.default %}selected{% endif %}>{{ opt }}</option>{% endfor %}
|
||||
</select>
|
||||
{% elif f.type == 'textarea' %}
|
||||
<textarea id="f-{{ f.name }}" name="{{ f.name }}" rows="4" placeholder="{{ f.placeholder }}" {% if f.required %}required{% endif %}>{{ f.default }}</textarea>
|
||||
{% elif f.type == 'checkbox' %}
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" id="f-{{ f.name }}" name="{{ f.name }}" value="yes"
|
||||
{% if f.default in ('yes', 'true', '1', 'on') %}checked{% endif %}>
|
||||
<span>启用</span>
|
||||
</label>
|
||||
{% elif f.type == 'number' %}
|
||||
<input type="number" id="f-{{ f.name }}" name="{{ f.name }}"
|
||||
value="{{ f.default }}" placeholder="{{ f.placeholder }}"
|
||||
min="{{ f.min or '' }}" max="{{ f.max or '' }}" step="{{ f.step or '' }}"
|
||||
{% if f.required %}required{% endif %}>
|
||||
{% elif f.type == 'password' %}
|
||||
<input type="password" id="f-{{ f.name }}" name="{{ f.name }}"
|
||||
value="{{ f.default }}" placeholder="{{ f.placeholder }}"
|
||||
autocomplete="new-password">
|
||||
{% else %}
|
||||
<input type="text" id="f-{{ f.name }}" name="{{ f.name }}"
|
||||
value="{{ f.default }}" placeholder="{{ f.placeholder }}"
|
||||
{% if f.required %}required{% endif %}>
|
||||
{% endif %}
|
||||
{% if f.help %}<p class="help">{{ f.help }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="btn-preview">👁 预览</button>
|
||||
<button type="button" class="btn" id="btn-download">💾 下载 .sh</button>
|
||||
<button type="button" class="btn btn-secondary" id="btn-add-bundle">📦 加入套餐</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="preview-card">
|
||||
<div class="preview-header">
|
||||
<h2>📄 脚本预览</h2>
|
||||
<div class="preview-actions">
|
||||
<button type="button" class="btn btn-small" id="btn-copy">📋 复制</button>
|
||||
<span class="muted" id="line-count">0 行</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="script-pre"><code>// 点击「👁 预览」生成脚本</code></pre>
|
||||
<div class="copy-toast" id="copy-toast">已复制</div>
|
||||
<div id="err-box" class="err-box hidden"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% if gen.post_steps or gen.verify_steps %}
|
||||
<section class="after">
|
||||
{% if gen.post_steps %}
|
||||
<h3>🛠 安装后操作</h3>
|
||||
<ol class="post-steps">
|
||||
{% for s in gen.post_steps %}<li><code>{{ s }}</code></li>{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
{% if gen.verify_steps %}
|
||||
<h3>✅ 验证</h3>
|
||||
<ol class="verify-steps">
|
||||
{% for s in gen.verify_steps %}<li><code>{{ s }}</code></li>{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<div class="bundle-drawer" id="bundle-drawer" hidden>
|
||||
<div class="bundle-header">
|
||||
<h3>📦 套餐 (multi-script bundle)</h3>
|
||||
<span id="bundle-count" class="muted">0 项</span>
|
||||
<button class="btn btn-small" id="btn-bundle-clear">清空</button>
|
||||
<button class="btn btn-small btn-primary" id="btn-bundle-download">下载 .zip</button>
|
||||
<button class="btn btn-small" id="btn-bundle-close">✕</button>
|
||||
</div>
|
||||
<ul id="bundle-list" class="bundle-list"></ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const GEN_ID = {{ gen.id | tojson }};
|
||||
const BUNDLE_KEY = "shell-gen-bundle";
|
||||
|
||||
function collectForm() {
|
||||
const form = document.getElementById("gen-form");
|
||||
const fd = new FormData(form);
|
||||
const params = {};
|
||||
for (const [k, v] of fd.entries()) {
|
||||
if (v !== "" || document.querySelector(`[name="${k}"]`).type === "checkbox") {
|
||||
params[k] = v;
|
||||
}
|
||||
}
|
||||
// unchecked checkboxes won't be in fd
|
||||
for (const el of form.querySelectorAll('input[type="checkbox"]')) {
|
||||
params[el.name] = el.checked ? "yes" : "no";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
async function preview() {
|
||||
const params = collectForm();
|
||||
const r = await fetch("/api/preview", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({generator: GEN_ID, params})
|
||||
});
|
||||
const d = await r.json();
|
||||
const pre = document.getElementById("script-pre");
|
||||
const err = document.getElementById("err-box");
|
||||
if (!d.ok) {
|
||||
err.textContent = d.error;
|
||||
err.classList.remove("hidden");
|
||||
pre.textContent = "// 错误: " + d.error;
|
||||
return;
|
||||
}
|
||||
err.classList.add("hidden");
|
||||
pre.innerHTML = "<code>" + escapeHtml(d.script) + "</code>";
|
||||
document.getElementById("line-count").textContent = d.script.split("\n").length + " 行";
|
||||
}
|
||||
|
||||
document.getElementById("gen-form").addEventListener("submit", e => { e.preventDefault(); preview(); });
|
||||
document.getElementById("btn-download").addEventListener("click", async () => {
|
||||
const params = collectForm();
|
||||
const r = await fetch("/api/download", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({generator: GEN_ID, params})
|
||||
});
|
||||
if (!r.ok) { alert("下载失败"); return; }
|
||||
const blob = await r.blob();
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = GEN_ID + ".sh";
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
});
|
||||
document.getElementById("btn-copy").addEventListener("click", () => {
|
||||
const txt = document.getElementById("script-pre").innerText;
|
||||
navigator.clipboard.writeText(txt).then(() => {
|
||||
const t = document.getElementById("copy-toast");
|
||||
t.classList.add("show");
|
||||
setTimeout(() => t.classList.remove("show"), 1500);
|
||||
});
|
||||
});
|
||||
|
||||
// Bundle drawer
|
||||
function loadBundle() {
|
||||
try { return JSON.parse(localStorage.getItem(BUNDLE_KEY) || "[]"); } catch { return []; }
|
||||
}
|
||||
function saveBundle(b) { localStorage.setItem(BUNDLE_KEY, JSON.stringify(b)); renderBundle(); }
|
||||
function renderBundle() {
|
||||
const list = loadBundle();
|
||||
document.getElementById("bundle-count").textContent = list.length + " 项";
|
||||
const ul = document.getElementById("bundle-list");
|
||||
if (list.length === 0) {
|
||||
ul.innerHTML = '<li class="empty">还没有添加任何生成器,点击「📦 加入套餐」加入。</li>';
|
||||
return;
|
||||
}
|
||||
ul.innerHTML = list.map((it, i) => `
|
||||
<li>
|
||||
<span class="b-id">${escapeHtml(it.generator)}</span>
|
||||
<span class="b-title">${escapeHtml(it.title || '')}</span>
|
||||
<button class="btn btn-small" data-rm="${i}">移除</button>
|
||||
</li>
|
||||
`).join("");
|
||||
ul.querySelectorAll("[data-rm]").forEach(b => {
|
||||
b.addEventListener("click", () => {
|
||||
const idx = parseInt(b.dataset.rm, 10);
|
||||
const items = loadBundle();
|
||||
items.splice(idx, 1);
|
||||
saveBundle(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
document.getElementById("btn-add-bundle").addEventListener("click", () => {
|
||||
const items = loadBundle();
|
||||
if (items.some(it => it.generator === GEN_ID)) return alert("已在套餐中");
|
||||
const titleMatch = document.querySelector(".gen-header h1")?.textContent || GEN_ID;
|
||||
items.push({generator: GEN_ID, params: collectForm(), title: titleMatch});
|
||||
saveBundle(items);
|
||||
const drawer = document.getElementById("bundle-drawer");
|
||||
drawer.hidden = false;
|
||||
});
|
||||
document.getElementById("btn-bundle-close").addEventListener("click", () => document.getElementById("bundle-drawer").hidden = true);
|
||||
document.getElementById("btn-bundle-clear").addEventListener("click", () => saveBundle([]));
|
||||
document.getElementById("btn-bundle-download").addEventListener("click", async () => {
|
||||
const items = loadBundle();
|
||||
if (items.length === 0) return alert("套餐为空");
|
||||
const r = await fetch("/api/bundle", {
|
||||
method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({items})
|
||||
});
|
||||
if (!r.ok) return alert("打包失败");
|
||||
const blob = await r.blob();
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = "shell-gen-bundle.zip";
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
});
|
||||
renderBundle();
|
||||
|
||||
// initial preview
|
||||
preview();
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
}[c]));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Shell-Gen · 首页{% endblock %}
|
||||
{% block body %}
|
||||
<main class="container">
|
||||
<section class="hero">
|
||||
<h1>选择需要部署的服务</h1>
|
||||
<p class="lead">填写参数,实时生成可立即执行的 bash 脚本。所有脚本会先做 distro 检测
|
||||
(Ubuntu/Debian/CentOS/RHEL/Rocky/Alma) 再安装,不会破坏现有服务。</p>
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="num">{{ total }}</div><div class="lbl">生成器</div></div>
|
||||
{% for cat in all_cats %}
|
||||
{% set n = all_cats|length %}
|
||||
{% endfor %}
|
||||
<div class="stat"><div class="num">{{ categories|length }}</div><div class="lbl">分类</div></div>
|
||||
<div class="stat"><div class="num">100%</div><div class="lbl">开源</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% for cat in categories %}
|
||||
<section class="cat-section" id="cat-{{ cat }}">
|
||||
<h2><span class="cat-icon">{{ category_icons.get(cat, '📦') }}</span> {{ category_labels.get(cat, cat) }}</h2>
|
||||
<div class="grid">
|
||||
{% for gid, gen in (request.application_generator_items if false else []) %}{% endfor %}
|
||||
{# Render via Python loop using all generators grouped by cat #}
|
||||
{% for gid, gen in [] %}{% endfor %}
|
||||
</div>
|
||||
<div class="grid" data-cat="{{ cat }}">
|
||||
{# Render in JS to keep templating simple #}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<section class="cat-section">
|
||||
<h2>📚 全部生成器</h2>
|
||||
<div class="grid" id="all-grid"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
fetch("/api/categories").then(r => r.json()).then(d => {
|
||||
// populate per-category sections
|
||||
for (const cat of d.categories) {
|
||||
const target = document.querySelector(`#cat-${cat.id} .grid[data-cat]`);
|
||||
if (target) target.innerHTML = cat.items.map(renderCard).join('');
|
||||
}
|
||||
// populate "全部生成器"
|
||||
const all = d.categories.flatMap(c => c.items);
|
||||
document.getElementById("all-grid").innerHTML = all.map(renderCard).join('');
|
||||
});
|
||||
|
||||
function renderCard(g) {
|
||||
const tags = (g.tags || []).slice(0, 4).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('');
|
||||
const os = (g.os || []).slice(0, 3).map(t => `<span class="os">${escapeHtml(t)}</span>`).join('');
|
||||
return `
|
||||
<a class="card" href="/gen/${g.id}">
|
||||
<div class="card-icon">${g.icon || '📦'}</div>
|
||||
<div class="card-title">${escapeHtml(g.title)}</div>
|
||||
<div class="card-desc">${escapeHtml(g.description)}</div>
|
||||
<div class="card-meta">
|
||||
<div class="tags">${tags}</div>
|
||||
<div class="oss">${os}</div>
|
||||
</div>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, c => ({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
}[c]));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user