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:
cnbug
2026-08-04 00:36:35 +08:00
commit 319f683a16
17 changed files with 4177 additions and 0 deletions
+33
View File
@@ -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>
+253
View File
@@ -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 => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
}
</script>
{% endblock %}
+72
View File
@@ -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 => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
}
</script>
{% endblock %}