mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-09-29 13:39:41 +00:00
Compare commits
58
Commits
Generated
+146
@@ -1,3 +1,149 @@
|
||||
## 2026-09-26
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Manyfold: make /opt/manyfold_data writable for the manyfold user [@sergstepanenko](https://github.com/sergstepanenko) ([#17534](https://github.com/community-scripts/ProxmoxVE/pull/17534))
|
||||
- steamcmd: update itself before installing Satisfactory [@MickLesk](https://github.com/MickLesk) ([#17526](https://github.com/community-scripts/ProxmoxVE/pull/17526))
|
||||
- AudioMuse-AI: build noavx2 environment on Python 3.11 [@MickLesk](https://github.com/MickLesk) ([#17528](https://github.com/community-scripts/ProxmoxVE/pull/17528))
|
||||
- LobeHub: pin lobehub/ui at 5.49 while LobeHub's stable release needs it [@MickLesk](https://github.com/MickLesk) ([#17531](https://github.com/community-scripts/ProxmoxVE/pull/17531))
|
||||
- Keep Trilium updates on the server releases [@MickLesk](https://github.com/MickLesk) ([#17525](https://github.com/community-scripts/ProxmoxVE/pull/17525))
|
||||
- NginxProxyManager: generate admin config and repair certbot's venv on update [@MickLesk](https://github.com/MickLesk) ([#17523](https://github.com/community-scripts/ProxmoxVE/pull/17523))
|
||||
- Komodo: add KOMODO_HOST | fix broken spinner [@MickLesk](https://github.com/MickLesk) ([#17481](https://github.com/community-scripts/ProxmoxVE/pull/17481))
|
||||
- romm: run rq cron and a scans worker for RomM 5.3 [@davidbb](https://github.com/davidbb) ([#17502](https://github.com/community-scripts/ProxmoxVE/pull/17502))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Semaphore: fix BoltDB migration by pinning 2.18.30 [@MickLesk](https://github.com/MickLesk) ([#17439](https://github.com/community-scripts/ProxmoxVE/pull/17439))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep a dash-led bridge comment out of whiptail's options [@MickLesk](https://github.com/MickLesk) ([core#74](https://github.com/community-scripts/core/pull/74))
|
||||
|
||||
## 2026-09-25
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- RustFS ([#17499](https://github.com/community-scripts/ProxmoxVE/pull/17499))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Ignis: take the Obsidian version from release tag [@MickLesk](https://github.com/MickLesk) ([#17503](https://github.com/community-scripts/ProxmoxVE/pull/17503))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Umbrel OS VM [@MickLesk](https://github.com/MickLesk) ([#17505](https://github.com/community-scripts/ProxmoxVE/pull/17505))
|
||||
- Docker-LXC: remove Portainer installation from install [@MickLesk](https://github.com/MickLesk) ([#17493](https://github.com/community-scripts/ProxmoxVE/pull/17493))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Incus: make prompts visible for end-user [@MickLesk](https://github.com/MickLesk) ([core#73](https://github.com/community-scripts/core/pull/73))
|
||||
- incus: do not let an install script block on an invisible prompt [@MickLesk](https://github.com/MickLesk) ([core#71](https://github.com/community-scripts/core/pull/71))
|
||||
- tel: report progress from installs only, not updates [@MickLesk](https://github.com/MickLesk) ([core#72](https://github.com/community-scripts/core/pull/72))
|
||||
|
||||
## 2026-09-24
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Declare the RomM filesystem structure required since 5.3.0 [@MickLesk](https://github.com/MickLesk) ([#17480](https://github.com/community-scripts/ProxmoxVE/pull/17480))
|
||||
- general: use apt_update_safe instead of aborting on a failed apt update [@MickLesk](https://github.com/MickLesk) ([#17462](https://github.com/community-scripts/ProxmoxVE/pull/17462))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Journiv: Serve Node Frontend and add HTTP Auth .env [@MickLesk](https://github.com/MickLesk) ([#17479](https://github.com/community-scripts/ProxmoxVE/pull/17479))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- core: accept an off-subnet gateway for /31 and /32 [@MickLesk](https://github.com/MickLesk) ([core#70](https://github.com/community-scripts/core/pull/70))
|
||||
|
||||
## 2026-09-23
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(signoz): install the histogramQuantile ClickHouse function [@maksimtech](https://github.com/maksimtech) ([#17386](https://github.com/community-scripts/ProxmoxVE/pull/17386))
|
||||
- omniroute: bump to 4 GB for the npm install [@MickLesk](https://github.com/MickLesk) ([#17461](https://github.com/community-scripts/ProxmoxVE/pull/17461))
|
||||
- Run Borg-UI with a single gunicorn worker [@MickLesk](https://github.com/MickLesk) ([#17445](https://github.com/community-scripts/ProxmoxVE/pull/17445))
|
||||
- immich: keep geodata linked and the build deps present on update [@MickLesk](https://github.com/MickLesk) ([#17438](https://github.com/community-scripts/ProxmoxVE/pull/17438))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Scanopy [@MickLesk](https://github.com/MickLesk) ([#16797](https://github.com/community-scripts/ProxmoxVE/pull/16797))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- stop a partial find from aborting the caller under pipefail [@MickLesk](https://github.com/MickLesk) ([core#68](https://github.com/community-scripts/core/pull/68))
|
||||
- core: give RHEL-family containers an ostype PVE accepts [@MickLesk](https://github.com/MickLesk) ([core#67](https://github.com/community-scripts/core/pull/67))
|
||||
- core: do not abort on a failed apt update [@MickLesk](https://github.com/MickLesk) ([core#66](https://github.com/community-scripts/core/pull/66))
|
||||
- VM's: report a finished VM as done, not aborted [@MickLesk](https://github.com/MickLesk) ([core#65](https://github.com/community-scripts/core/pull/65))
|
||||
- VM-Core: decide console autologin from the image, not USE_CLOUD_INIT [@MickLesk](https://github.com/MickLesk) ([core#63](https://github.com/community-scripts/core/pull/63))
|
||||
- VM-Core: report failed image steps and restore tty1 autologin without cloud-init [@MickLesk](https://github.com/MickLesk) ([core#62](https://github.com/community-scripts/core/pull/62))
|
||||
- tools: raise pnpm/yarn fetch timeout and lower network concurrency [@MickLesk](https://github.com/MickLesk) ([core#59](https://github.com/community-scripts/core/pull/59))
|
||||
- VM-Core: stop discarding cloud-init error messages [@MickLesk](https://github.com/MickLesk) ([core#60](https://github.com/community-scripts/core/pull/60))
|
||||
|
||||
## 2026-09-22
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(invoiceshelf): re-link storage during update [@owenvoke](https://github.com/owenvoke) ([#17431](https://github.com/community-scripts/ProxmoxVE/pull/17431))
|
||||
- fix(alpine-it-tools): write the version file after a successful update [@Houtek](https://github.com/Houtek) ([#17421](https://github.com/community-scripts/ProxmoxVE/pull/17421))
|
||||
- Umami: Align pnpm with engines.pnpm on update [@ReneNulschDE](https://github.com/ReneNulschDE) ([#17404](https://github.com/community-scripts/ProxmoxVE/pull/17404))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- several scripts: let uv see the project before syncing it | refactor some scripts that use uv [@MickLesk](https://github.com/MickLesk) ([#17436](https://github.com/community-scripts/ProxmoxVE/pull/17436))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- setup_uv: honour a project's required-version pin [@MickLesk](https://github.com/MickLesk) ([core#58](https://github.com/community-scripts/core/pull/58))
|
||||
- fix(motd): repair legacy 00_lxc-details.sh header and dead color placeholders [@BadFlo](https://github.com/BadFlo) ([core#53](https://github.com/community-scripts/core/pull/53))
|
||||
- meilisearch: take the raw binary, not the .deb [@MickLesk](https://github.com/MickLesk) ([core#57](https://github.com/community-scripts/core/pull/57))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- github: Stop generating header filenames with a newline in them [@MickLesk](https://github.com/MickLesk) ([#17435](https://github.com/community-scripts/ProxmoxVE/pull/17435))
|
||||
|
||||
### ❔ Uncategorized
|
||||
|
||||
- chore(ct): sync cloudflare-ddns defaults with PocketBase [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17446](https://github.com/community-scripts/ProxmoxVE/pull/17446))
|
||||
|
||||
## 2026-09-21
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- AudioMuse-AI ([#17415](https://github.com/community-scripts/ProxmoxVE/pull/17415))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(portabase): meet the default admin password rules [@maksimtech](https://github.com/maksimtech) ([#17384](https://github.com/community-scripts/ProxmoxVE/pull/17384))
|
||||
- truenas-vm: strip the G before the storage allocation [@MickLesk](https://github.com/MickLesk) ([#17407](https://github.com/community-scripts/ProxmoxVE/pull/17407))
|
||||
- Librenms - Fix local snmpd community string setup [@dopch](https://github.com/dopch) ([#17363](https://github.com/community-scripts/ProxmoxVE/pull/17363))
|
||||
- fix(iventoy): preserve release data and migrate legacy service launchers [@Msprg](https://github.com/Msprg) ([#17161](https://github.com/community-scripts/ProxmoxVE/pull/17161))
|
||||
- xyops: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17398](https://github.com/community-scripts/ProxmoxVE/pull/17398))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Generate app headers [@github-actions[bot]](https://github.com/github-actions[bot]) ([core#56](https://github.com/community-scripts/core/pull/56))
|
||||
- github-api: take the release the maintainer marked as latest [@MickLesk](https://github.com/MickLesk) ([core#55](https://github.com/community-scripts/core/pull/55))
|
||||
- general: prune old Rust toolchains [@MickLesk](https://github.com/MickLesk) ([core#52](https://github.com/community-scripts/core/pull/52))
|
||||
- Ask when the preselected storage cannot hold the content [@MickLesk](https://github.com/MickLesk) ([core#51](https://github.com/community-scripts/core/pull/51))
|
||||
|
||||
## 2026-09-20
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Your-Spotify ([#17376](https://github.com/community-scripts/ProxmoxVE/pull/17376))
|
||||
|
||||
## 2026-09-19
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
Generated
-626
@@ -1,626 +0,0 @@
|
||||
name: PocketBase AI Bot
|
||||
|
||||
# Natural-language companion to pocketbase-bot.yml.
|
||||
# Mention the bot in plain English, e.g.:
|
||||
# @pocketbase-bot change RAM to 4096 on zigbee2mqtt
|
||||
# @pocketbase-bot disable script Nextcloud because upstream is broken
|
||||
# The bot parses the request with GitHub Models, replies with the exact change(s)
|
||||
# it understood, and only applies them after you reply "@pocketbase-bot confirm".
|
||||
# The slash-command bot (/pocketbase ...) is unaffected; triggers do not overlap.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
models: read # lets the built-in GITHUB_TOKEN call GitHub Models inference
|
||||
contents: write # built-in token opens the CT-defaults sync PR (like the slash bot)
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
ai-bot:
|
||||
runs-on: self-hosted
|
||||
# Broad gate; the script does precise keyword + self-author checks.
|
||||
if: contains(github.event.comment.body, '@pocketbase-bot')
|
||||
|
||||
steps:
|
||||
- name: Mint GitHub App token (bot identity)
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ vars.GHAPP_PBBOT_ID }}
|
||||
private-key: ${{ secrets.GHAPP_PBBOT_PRIVATE_KEY }}
|
||||
|
||||
- name: Run PocketBase AI bot
|
||||
env:
|
||||
# GitHub REST as the bot identity
|
||||
GH_APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
PB_BOT_APP_ID: ${{ vars.GHAPP_PBBOT_ID }}
|
||||
# GitHub Models inference uses the built-in token (needs models: read)
|
||||
MODELS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Built-in token for git/PR ops (CT-defaults sync), mirroring the slash bot
|
||||
GH_DEFAULT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_MODEL: openai/gpt-4o
|
||||
# PocketBase
|
||||
POCKETBASE_URL: ${{ vars.POCKETBASE_URL }}
|
||||
POCKETBASE_COLLECTION: ${{ vars.POCKETBASE_COLLECTION }}
|
||||
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
|
||||
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||
FRONTEND_URL: ${{ vars.FRONTEND_URL }}
|
||||
REVALIDATE_SECRET: ${{ secrets.FRONTEND_INGEST_SECRET }}
|
||||
# Event context
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
COMMENT_AUTHOR_TYPE: ${{ github.event.comment.user.type }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
ACTOR: ${{ github.event.comment.user.login }}
|
||||
ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function () {
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
|
||||
// ── HTTP helper with redirect following ────────────────────────────
|
||||
function request(fullUrl, opts, redirectCount) {
|
||||
redirectCount = redirectCount || 0;
|
||||
return new Promise(function (resolve, reject) {
|
||||
const u = url.parse(fullUrl);
|
||||
const isHttps = u.protocol === 'https:';
|
||||
const body = opts.body;
|
||||
const options = {
|
||||
hostname: u.hostname,
|
||||
port: u.port || (isHttps ? 443 : 80),
|
||||
path: u.path,
|
||||
method: opts.method || 'GET',
|
||||
headers: opts.headers || {}
|
||||
};
|
||||
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
const lib = isHttps ? https : http;
|
||||
const req = lib.request(options, function (res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
|
||||
const redirectUrl = url.resolve(fullUrl, res.headers.location);
|
||||
res.resume();
|
||||
resolve(request(redirectUrl, opts, redirectCount + 1));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', function (chunk) { data += chunk; });
|
||||
res.on('end', function () {
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── GitHub REST (as the bot app) ───────────────────────────────────
|
||||
const owner = process.env.REPO_OWNER;
|
||||
const repo = process.env.REPO_NAME;
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
|
||||
const commentId = parseInt(process.env.COMMENT_ID, 10);
|
||||
const actor = process.env.ACTOR;
|
||||
|
||||
function ghRequest(path, method, body) {
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GH_APP_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'PocketBase-AI-Bot'
|
||||
};
|
||||
const bodyStr = body ? JSON.stringify(body) : undefined;
|
||||
if (bodyStr) headers['Content-Type'] = 'application/json';
|
||||
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
|
||||
}
|
||||
|
||||
// Same as ghRequest but authenticated with the built-in GITHUB_TOKEN.
|
||||
// Used for the CT-defaults sync branch/PR (the App token lacks contents:write).
|
||||
function ghDefault(path, method, body) {
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GH_DEFAULT_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'PocketBase-AI-Bot'
|
||||
};
|
||||
const bodyStr = body ? JSON.stringify(body) : undefined;
|
||||
if (bodyStr) headers['Content-Type'] = 'application/json';
|
||||
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
|
||||
}
|
||||
|
||||
async function addReaction(content) {
|
||||
try {
|
||||
await ghRequest('/repos/' + owner + '/' + repo + '/issues/comments/' + commentId + '/reactions', 'POST', { content });
|
||||
} catch (e) { console.warn('Could not add reaction:', e.message); }
|
||||
}
|
||||
async function postComment(text) {
|
||||
const res = await ghRequest('/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments', 'POST', { body: text });
|
||||
if (!res.ok) console.warn('Could not post comment:', res.body);
|
||||
return res.ok ? JSON.parse(res.body) : null;
|
||||
}
|
||||
async function updateComment(id, text) {
|
||||
const res = await ghRequest('/repos/' + owner + '/' + repo + '/issues/comments/' + id, 'PATCH', { body: text });
|
||||
if (!res.ok) console.warn('Could not update comment:', res.body);
|
||||
}
|
||||
async function listIssueComments() {
|
||||
const all = [];
|
||||
let page = 1;
|
||||
while (page <= 10) {
|
||||
const res = await ghRequest('/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments?per_page=100&page=' + page);
|
||||
if (!res.ok) break;
|
||||
const batch = JSON.parse(res.body);
|
||||
all.push.apply(all, batch);
|
||||
if (batch.length < 100) break;
|
||||
page++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
// ── 1. Self-trigger guard (App-token comments DO re-fire this event) ─
|
||||
if (process.env.COMMENT_AUTHOR_TYPE === 'Bot') {
|
||||
console.log('Comment authored by a bot — skipping to avoid loops.');
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 2. Permission gate (mirrors the slash bot) ─────────────────────
|
||||
const association = process.env.ACTOR_ASSOCIATION;
|
||||
if (association !== 'OWNER' && association !== 'MEMBER') {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase AI Bot**: @' + actor + ' is not authorized to use this command.\n' +
|
||||
'Only org members (Contributors team) can use `@pocketbase-bot`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 3. Extract the instruction after the @pocketbase-bot handle ────
|
||||
const commentBody = process.env.COMMENT_BODY || '';
|
||||
const handleMatch = commentBody.match(/@pocketbase-bot(\[bot\])?/i);
|
||||
if (!handleMatch) {
|
||||
console.log('No @pocketbase-bot handle found — ignoring.');
|
||||
return;
|
||||
}
|
||||
const instruction = commentBody.slice(handleMatch.index + handleMatch[0].length).trim();
|
||||
if (!instruction) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'ℹ️ **PocketBase AI Bot**: Tell me what to do, e.g.\n' +
|
||||
'`@pocketbase-bot change RAM to 4096 on zigbee2mqtt` or `@pocketbase-bot disable script Nextcloud`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── PocketBase auth + low-level helpers ────────────────────────────
|
||||
const pbRaw = process.env.POCKETBASE_URL.replace(/\/$/, '');
|
||||
const apiBase = /\/api$/i.test(pbRaw) ? pbRaw : pbRaw + '/api';
|
||||
const coll = process.env.POCKETBASE_COLLECTION;
|
||||
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
|
||||
|
||||
async function pbAuth() {
|
||||
const res = await request(apiBase + '/collections/users/auth-with-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identity: process.env.POCKETBASE_ADMIN_EMAIL, password: process.env.POCKETBASE_ADMIN_PASSWORD })
|
||||
});
|
||||
if (!res.ok) throw new Error('PocketBase auth failed: ' + res.body);
|
||||
return JSON.parse(res.body).token;
|
||||
}
|
||||
async function pbFindRecord(token, slug) {
|
||||
const filter = "(slug='" + String(slug).replace(/'/g, "''") + "')";
|
||||
const res = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', { headers: { 'Authorization': token } });
|
||||
const list = JSON.parse(res.body);
|
||||
return list.items && list.items[0];
|
||||
}
|
||||
async function pbPatch(token, id, payload) {
|
||||
return request(recordsUrl + '/' + id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
function readJsonBlob(val) {
|
||||
if (Array.isArray(val)) return val;
|
||||
try { return JSON.parse(val || '[]'); } catch (e) { return []; }
|
||||
}
|
||||
async function revalidate(s) {
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
const secret = process.env.REVALIDATE_SECRET;
|
||||
if (!frontendUrl || !secret) return;
|
||||
try {
|
||||
await request(frontendUrl.replace(/\/$/, '') + '/api/revalidate', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + secret, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tags: ['scripts', 'script-' + s] })
|
||||
});
|
||||
} catch (e) { console.warn('Revalidation skipped:', e.message); }
|
||||
}
|
||||
|
||||
// ── CT-defaults sync PR (copied from slash bot) ────────────────────
|
||||
function encodeContentPath(filePath) { return filePath.split('/').map(encodeURIComponent).join('/'); }
|
||||
function decodeGitHubContent(content) { return Buffer.from((content || '').replace(/\n/g, ''), 'base64').toString('utf8'); }
|
||||
function sanitizeBranchPart(value) {
|
||||
return (value || '').toLowerCase().replace(/[^a-z0-9._/-]+/g, '-').replace(/\/+/g, '/').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
function applyCtDefaultChanges(scriptText, varChanges) {
|
||||
let nextText = scriptText;
|
||||
const updatedVars = [], unchangedVars = [];
|
||||
for (const [varName, rawValue] of Object.entries(varChanges)) {
|
||||
const newValue = String(rawValue);
|
||||
const pattern = new RegExp('(^\\s*' + varName + '="\\$\\{' + varName + ':-)([^"}]*)(\\}"\\s*$)', 'm');
|
||||
const match = nextText.match(pattern);
|
||||
if (!match) continue;
|
||||
if (match[2] === newValue) { unchangedVars.push(varName); continue; }
|
||||
nextText = nextText.replace(pattern, '$1' + newValue + '$3');
|
||||
updatedVars.push(varName);
|
||||
}
|
||||
return { nextText, updatedVars, unchangedVars };
|
||||
}
|
||||
async function ensureBranch(defaultBranch, branchName) {
|
||||
const ghRequest = ghDefault; // git ops run as the built-in token
|
||||
const branchRefRes = await ghRequest('/repos/' + owner + '/' + repo + '/git/ref/heads/' + encodeURIComponent(branchName));
|
||||
if (branchRefRes.ok) return;
|
||||
const defaultRefRes = await ghRequest('/repos/' + owner + '/' + repo + '/git/ref/heads/' + encodeURIComponent(defaultBranch));
|
||||
if (!defaultRefRes.ok) throw new Error('Could not read default branch ref: ' + defaultRefRes.body);
|
||||
const defaultRef = JSON.parse(defaultRefRes.body);
|
||||
const createBranchRes = await ghRequest('/repos/' + owner + '/' + repo + '/git/refs', 'POST', { ref: 'refs/heads/' + branchName, sha: defaultRef.object.sha });
|
||||
if (!createBranchRes.ok) throw new Error('Could not create branch: ' + createBranchRes.body);
|
||||
}
|
||||
async function upsertCtDefaultsPr(slugValue, varChanges) {
|
||||
const ghRequest = ghDefault; // contents/PR ops run as the built-in token
|
||||
const wantedEntries = Object.entries(varChanges || {}).filter(function ([, v]) { return v !== undefined && v !== null && String(v) !== ''; });
|
||||
if (wantedEntries.length === 0) return { status: 'skipped', reason: 'No mapped CT defaults changed.' };
|
||||
const repoRes = await ghRequest('/repos/' + owner + '/' + repo);
|
||||
if (!repoRes.ok) throw new Error('Could not read repository metadata: ' + repoRes.body);
|
||||
const defaultBranch = JSON.parse(repoRes.body).default_branch;
|
||||
const ctPath = 'ct/' + slugValue + '.sh';
|
||||
const encodedCtPath = encodeContentPath(ctPath);
|
||||
const defaultFileRes = await ghRequest('/repos/' + owner + '/' + repo + '/contents/' + encodedCtPath + '?ref=' + encodeURIComponent(defaultBranch));
|
||||
if (defaultFileRes.statusCode === 404) return { status: 'skipped', reason: 'No matching CT file found at `' + ctPath + '`.' };
|
||||
if (!defaultFileRes.ok) throw new Error('Could not read CT file from default branch: ' + defaultFileRes.body);
|
||||
const branchName = 'pocketbase-sync/' + sanitizeBranchPart(slugValue || 'unknown');
|
||||
await ensureBranch(defaultBranch, branchName);
|
||||
const branchFileRes = await ghRequest('/repos/' + owner + '/' + repo + '/contents/' + encodedCtPath + '?ref=' + encodeURIComponent(branchName));
|
||||
if (!branchFileRes.ok) throw new Error('Could not read CT file from sync branch: ' + branchFileRes.body);
|
||||
const branchFile = JSON.parse(branchFileRes.body);
|
||||
const currentBranchText = decodeGitHubContent(branchFile.content);
|
||||
const updateResult = applyCtDefaultChanges(currentBranchText, Object.fromEntries(wantedEntries));
|
||||
if (updateResult.updatedVars.length === 0) return { status: 'skipped', reason: 'CT defaults already up to date.' };
|
||||
const putRes = await ghRequest('/repos/' + owner + '/' + repo + '/contents/' + encodedCtPath, 'PUT', {
|
||||
message: 'chore(ct): sync ' + slugValue + ' defaults from PocketBase',
|
||||
content: Buffer.from(updateResult.nextText, 'utf8').toString('base64'),
|
||||
sha: branchFile.sha,
|
||||
branch: branchName
|
||||
});
|
||||
if (!putRes.ok) throw new Error('Could not update CT file: ' + putRes.body);
|
||||
const openPrRes = await ghRequest('/repos/' + owner + '/' + repo + '/pulls?state=open&head=' + encodeURIComponent(owner + ':' + branchName) + '&base=' + encodeURIComponent(defaultBranch));
|
||||
if (!openPrRes.ok) throw new Error('Could not query existing PRs: ' + openPrRes.body);
|
||||
const openPrs = JSON.parse(openPrRes.body);
|
||||
if (openPrs.length > 0) return { status: 'updated', prUrl: openPrs[0].html_url, updatedVars: updateResult.updatedVars };
|
||||
const createPrRes = await ghRequest('/repos/' + owner + '/' + repo + '/pulls', 'POST', {
|
||||
title: 'chore(ct): sync ' + slugValue + ' defaults with PocketBase',
|
||||
body: '## Summary\n- Sync default CT variables for `' + slugValue + '` after an `@pocketbase-bot` update.\n- Updated vars: `' + updateResult.updatedVars.join('`, `') + '`.\n\n## Source\n- Triggered by @' + actor + ' via PocketBase AI bot.\n',
|
||||
head: branchName,
|
||||
base: defaultBranch
|
||||
});
|
||||
if (!createPrRes.ok) throw new Error('Could not create PR: ' + createPrRes.body);
|
||||
return { status: 'created', prUrl: JSON.parse(createPrRes.body).html_url, updatedVars: updateResult.updatedVars };
|
||||
}
|
||||
|
||||
// ── Allow-lists (mirror the slash bot) ─────────────────────────────
|
||||
const ALLOWED_FIELDS = {
|
||||
name: 'string', description: 'string', logo: 'string', documentation: 'string',
|
||||
website: 'string', project_url: 'string', repository: 'string', config_path: 'string',
|
||||
tags: 'string', port: 'number', default_user: 'nullable_string', default_passwd: 'nullable_string',
|
||||
unprivileged: 'number', updateable: 'boolean', privileged: 'boolean',
|
||||
architectures: 'select_list', platforms: 'select_list',
|
||||
is_dev: 'boolean', is_disabled: 'boolean', disable_message: 'string',
|
||||
is_deleted: 'boolean', deleted_message: 'string'
|
||||
};
|
||||
const SELECT_VALUES = {
|
||||
architectures: ['amd64', 'arm64'],
|
||||
platforms: ['pve', 'incus'],
|
||||
};
|
||||
const FIELD_TO_CT_VAR = { tags: 'var_tags', unprivileged: 'var_unprivileged' };
|
||||
const RESOURCE_KEYS = { cpu: 'number', ram: 'number', hdd: 'number', os: 'string', version: 'string' };
|
||||
const METHOD_KEYS = { config_path: 'string', script: 'string' };
|
||||
const ALL_METHOD_KEYS = Object.assign({}, RESOURCE_KEYS, METHOD_KEYS);
|
||||
const RESOURCE_TO_CT_VAR = { cpu: 'var_cpu', ram: 'var_ram', hdd: 'var_disk', os: 'var_os', version: 'var_version' };
|
||||
|
||||
function castFieldValue(key, rawVal) {
|
||||
const type = ALLOWED_FIELDS[key];
|
||||
if (!type) return { error: 'Unknown field `' + key + '`' };
|
||||
if (type === 'boolean') {
|
||||
if (rawVal === true || rawVal === 'true') return { value: true };
|
||||
if (rawVal === false || rawVal === 'false') return { value: false };
|
||||
return { error: '`' + key + '` must be true/false' };
|
||||
}
|
||||
if (type === 'number') {
|
||||
const n = parseInt(rawVal, 10);
|
||||
if (isNaN(n)) return { error: '`' + key + '` must be a number' };
|
||||
return { value: n };
|
||||
}
|
||||
if (type === 'select_list') {
|
||||
// architectures/platforms are multi-selects over a closed set,
|
||||
// so an unknown value is rejected rather than stored. The model
|
||||
// may hand back an array or a comma-separated string.
|
||||
const allowed = SELECT_VALUES[key] || [];
|
||||
const raw = Array.isArray(rawVal) ? rawVal : String(rawVal == null ? '' : rawVal).split(',');
|
||||
const values = raw.map(function (v) { return String(v).trim(); }).filter(Boolean);
|
||||
const bad = values.filter(function (v) { return allowed.indexOf(v) === -1; });
|
||||
if (bad.length > 0) return { error: '`' + key + '` accepts ' + allowed.join(', ') + ' — got: ' + bad.join(', ') };
|
||||
return { value: values };
|
||||
}
|
||||
if (type === 'nullable_string') return { value: rawVal === '' || rawVal == null ? null : String(rawVal) };
|
||||
return { value: String(rawVal) };
|
||||
}
|
||||
|
||||
// ── Operation validation (used at propose AND confirm time) ────────
|
||||
// Never trust raw operations: enforce the field/op allow-lists and
|
||||
// re-cast values. Returns only well-formed, allowed operations.
|
||||
function sanitizeOperations(ops) {
|
||||
const validOps = [], problems = [];
|
||||
for (const op of (Array.isArray(ops) ? ops : [])) {
|
||||
if (op && op.kind === 'field') {
|
||||
const cast = castFieldValue(op.field, op.value);
|
||||
if (cast.error) { problems.push(cast.error); continue; }
|
||||
validOps.push({ kind: 'field', field: op.field, value: cast.value });
|
||||
} else if (op && op.kind === 'note' && ['add', 'edit', 'remove'].includes(op.action)) {
|
||||
validOps.push({ kind: 'note', action: op.action, type: String(op.type || ''), text: op.text, newText: op.newText });
|
||||
} else if (op && op.kind === 'method' && ['add', 'edit', 'remove'].includes(op.action)) {
|
||||
const changes = {};
|
||||
for (const [k, v] of Object.entries(op.changes || {})) { if (ALL_METHOD_KEYS[k]) changes[k] = v; }
|
||||
validOps.push({ kind: 'method', action: op.action, type: String(op.type || 'default'), changes });
|
||||
} else {
|
||||
problems.push('Unsupported operation: `' + JSON.stringify(op) + '`');
|
||||
}
|
||||
}
|
||||
return { validOps, problems };
|
||||
}
|
||||
|
||||
// ── Executor: apply a validated {slug, operations} set ─────────────
|
||||
async function applyOperations(action) {
|
||||
const token = await pbAuth();
|
||||
const record = await pbFindRecord(token, action.slug);
|
||||
if (!record) return { ok: false, summary: '❌ No PocketBase record for slug `' + action.slug + '`.' };
|
||||
|
||||
const fieldPayload = {};
|
||||
let notesArr = readJsonBlob(record.notes);
|
||||
let methodsArr = readJsonBlob(record.install_methods);
|
||||
let notesChanged = false, methodsChanged = false;
|
||||
const ctChanges = {};
|
||||
const lines = [];
|
||||
|
||||
for (const op of action.operations) {
|
||||
if (op.kind === 'field') {
|
||||
const cast = castFieldValue(op.field, op.value);
|
||||
if (cast.error) { lines.push('- ⚠️ skipped field: ' + cast.error); continue; }
|
||||
fieldPayload[op.field] = cast.value;
|
||||
if (FIELD_TO_CT_VAR[op.field]) ctChanges[FIELD_TO_CT_VAR[op.field]] = cast.value;
|
||||
lines.push('- `' + op.field + '` → `' + JSON.stringify(cast.value) + '`');
|
||||
} else if (op.kind === 'note') {
|
||||
const type = String(op.type || '').toLowerCase();
|
||||
if (op.action === 'add') {
|
||||
notesArr.push({ type, text: String(op.text || '') });
|
||||
notesChanged = true; lines.push('- note add `' + type + '`: ' + op.text);
|
||||
} else if (op.action === 'remove') {
|
||||
const before = notesArr.length;
|
||||
notesArr = notesArr.filter(function (n) { return !(String(n.type).toLowerCase() === type && n.text === op.text); });
|
||||
if (notesArr.length !== before) { notesChanged = true; lines.push('- note remove `' + type + '`: ' + op.text); }
|
||||
else lines.push('- ⚠️ note remove: no `' + type + '` note matched');
|
||||
} else if (op.action === 'edit') {
|
||||
const idx = notesArr.findIndex(function (n) { return String(n.type).toLowerCase() === type && n.text === op.text; });
|
||||
if (idx !== -1) { notesArr[idx].text = String(op.newText || ''); notesChanged = true; lines.push('- note edit `' + type + '`'); }
|
||||
else lines.push('- ⚠️ note edit: no `' + type + '` note matched');
|
||||
}
|
||||
} else if (op.kind === 'method') {
|
||||
const type = String(op.type || '').toLowerCase();
|
||||
const changes = op.changes || {};
|
||||
if (op.action === 'remove') {
|
||||
const before = methodsArr.length;
|
||||
methodsArr = methodsArr.filter(function (im) { return String(im.type || '').toLowerCase() !== type; });
|
||||
if (methodsArr.length !== before) { methodsChanged = true; lines.push('- method remove `' + type + '`'); }
|
||||
else lines.push('- ⚠️ method remove: `' + type + '` not found');
|
||||
} else {
|
||||
let method = methodsArr.find(function (im) { return String(im.type || '').toLowerCase() === type; });
|
||||
if (!method && op.action === 'add') { method = { type, resources: { cpu: 1, ram: 512, hdd: 4, os: 'debian', version: '13' } }; methodsArr.push(method); }
|
||||
if (!method) { lines.push('- ⚠️ method edit: `' + type + '` not found'); continue; }
|
||||
if (!method.resources) method.resources = {};
|
||||
for (const [k, v] of Object.entries(changes)) {
|
||||
if (RESOURCE_KEYS[k]) {
|
||||
method.resources[k] = RESOURCE_KEYS[k] === 'number' ? parseInt(v, 10) : String(v);
|
||||
if (RESOURCE_TO_CT_VAR[k]) ctChanges[RESOURCE_TO_CT_VAR[k]] = method.resources[k];
|
||||
} else if (METHOD_KEYS[k]) {
|
||||
method[k] = v === '' ? null : String(v);
|
||||
}
|
||||
}
|
||||
methodsChanged = true;
|
||||
lines.push('- method `' + (op.action === 'add' ? 'add' : 'edit') + '` `' + type + '`: ' + JSON.stringify(changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(fieldPayload).length) {
|
||||
const r = await pbPatch(token, record.id, fieldPayload);
|
||||
if (!r.ok) return { ok: false, summary: '❌ Field update failed:\n```\n' + r.body + '\n```' };
|
||||
}
|
||||
if (notesChanged) {
|
||||
const r = await pbPatch(token, record.id, { notes: notesArr });
|
||||
if (!r.ok) return { ok: false, summary: '❌ Notes update failed:\n```\n' + r.body + '\n```' };
|
||||
}
|
||||
if (methodsChanged) {
|
||||
const r = await pbPatch(token, record.id, { install_methods: methodsArr });
|
||||
if (!r.ok) return { ok: false, summary: '❌ Install-method update failed:\n```\n' + r.body + '\n```' };
|
||||
}
|
||||
await revalidate(action.slug);
|
||||
|
||||
let ctNote = '';
|
||||
if (Object.keys(ctChanges).length) {
|
||||
try {
|
||||
const sync = await upsertCtDefaultsPr(action.slug, ctChanges);
|
||||
if (sync.status === 'created') ctNote = '\n\n**CT sync PR:** ' + sync.prUrl;
|
||||
else if (sync.status === 'updated') ctNote = '\n\n**CT sync PR updated:** ' + sync.prUrl;
|
||||
else if (sync.status === 'skipped') ctNote = '\n\n**CT sync skipped:** ' + sync.reason;
|
||||
} catch (e) { ctNote = '\n\n**CT sync failed:** ' + e.message; }
|
||||
}
|
||||
return { ok: true, summary: lines.join('\n') + ctNote };
|
||||
}
|
||||
|
||||
// ── 4. Confirm branch ──────────────────────────────────────────────
|
||||
const PENDING_RE = /<!--\s*pocketbase-pending:\s*([A-Za-z0-9+/=]+)\s*-->/;
|
||||
const isConfirm = /^(confirm|yes|apply|do it|y)\b/i.test(instruction);
|
||||
|
||||
if (isConfirm) {
|
||||
const comments = await listIssueComments();
|
||||
const appId = String(process.env.PB_BOT_APP_ID || '');
|
||||
let pending = null, pendingComment = null;
|
||||
for (let i = comments.length - 1; i >= 0; i--) {
|
||||
const c = comments[i];
|
||||
// Only trust a marker in a comment THIS bot app authored — otherwise a
|
||||
// user could hand-craft a forged pocketbase-pending marker and confirm it.
|
||||
const byBotApp = c.user && c.user.type === 'Bot' &&
|
||||
c.performed_via_github_app && String(c.performed_via_github_app.id) === appId;
|
||||
if (!byBotApp) continue;
|
||||
const m = c.body && c.body.match(PENDING_RE);
|
||||
if (m) { pending = m[1]; pendingComment = c; break; }
|
||||
}
|
||||
if (!pending) {
|
||||
await addReaction('confused');
|
||||
await postComment('🤔 **PocketBase AI Bot**: I have no pending change to confirm in this thread.');
|
||||
return;
|
||||
}
|
||||
let action;
|
||||
try { action = JSON.parse(Buffer.from(pending, 'base64').toString('utf8')); }
|
||||
catch (e) { await postComment('❌ **PocketBase AI Bot**: Could not decode the pending change.'); return; }
|
||||
|
||||
// Re-validate the decoded operations before applying (defense-in-depth).
|
||||
const recheck = sanitizeOperations(action.operations);
|
||||
if (!action.slug || recheck.validOps.length === 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase AI Bot**: The pending change is no longer valid. Please restate the request.');
|
||||
return;
|
||||
}
|
||||
action.operations = recheck.validOps;
|
||||
|
||||
let result;
|
||||
try { result = await applyOperations(action); }
|
||||
catch (e) { await addReaction('-1'); await postComment('❌ **PocketBase AI Bot**: ' + e.message); return; }
|
||||
|
||||
if (!result.ok) { await addReaction('-1'); await postComment(result.summary); return; }
|
||||
await updateComment(pendingComment.id, pendingComment.body.replace(PENDING_RE, '<!-- pocketbase-applied -->'));
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase AI Bot**: Applied to **`' + action.slug + '`**\n\n' + result.summary +
|
||||
'\n\n*Confirmed by @' + actor + '*'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 5. New request: acknowledge, fetch script list, call the model ─
|
||||
await addReaction('eyes');
|
||||
|
||||
const token0 = await pbAuth();
|
||||
const scripts = [];
|
||||
let page = 1;
|
||||
while (page <= 5) {
|
||||
const res = await request(recordsUrl + '?fields=slug,name&perPage=500&page=' + page, { headers: { 'Authorization': token0 } });
|
||||
if (!res.ok) break;
|
||||
const data = JSON.parse(res.body);
|
||||
(data.items || []).forEach(function (it) { if (it.slug) scripts.push({ slug: it.slug, name: it.name || it.slug }); });
|
||||
if (!data.items || data.items.length < 500) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
'You translate a maintainer\'s natural-language request into a STRICT JSON change-set for a ' +
|
||||
'script catalog (PocketBase). Respond with a SINGLE JSON object and nothing else.\n\n' +
|
||||
'Schema:\n' +
|
||||
'{\n' +
|
||||
' "slug": string|null, // MUST be one of the known slugs below, chosen from the request\n' +
|
||||
' "operations": [ ... ], // [] if you cannot determine concrete changes\n' +
|
||||
' "human_summary": string, // short plain-English description\n' +
|
||||
' "clarification": string|null // a question to ask if ambiguous/unsupported; else null\n' +
|
||||
'}\n\n' +
|
||||
'Operation kinds:\n' +
|
||||
'- {"kind":"field","field":<one of: ' + Object.keys(ALLOWED_FIELDS).join(', ') + '>,"value":<bool|number|string>}\n' +
|
||||
' (booleans true/false; "is_disabled"/"is_deleted"/"is_dev" are booleans; "port"/"unprivileged" are numbers; rest strings.)\n' +
|
||||
'- {"kind":"note","action":"add"|"edit"|"remove","type":string,"text":string,"newText":string?}\n' +
|
||||
'- {"kind":"method","action":"add"|"edit"|"remove","type":string,"changes":{cpu?:number,ram?:number,hdd?:number,os?:string,version?:string,config_path?:string,script?:string}}\n' +
|
||||
' (RAM/HDD are in MB/GB; method "type" defaults to "default" if the user does not name one.)\n\n' +
|
||||
'Rules:\n' +
|
||||
'- Only use fields/operations listed above. If the request needs something else, set clarification and operations=[].\n' +
|
||||
'- "disable"/"enable" map to is_disabled true/false. If disabling and the user gave a reason, also set disable_message.\n' +
|
||||
'- Resolve the target script to a slug from the list. If you cannot confidently match exactly one, set slug=null and ask via clarification.\n\n' +
|
||||
'Known scripts (slug — name):\n' +
|
||||
scripts.map(function (s) { return s.slug + ' — ' + s.name; }).join('\n');
|
||||
|
||||
const modelRes = await request('https://models.github.ai/inference/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + process.env.MODELS_TOKEN, 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: process.env.AI_MODEL || 'openai/gpt-4o',
|
||||
temperature: 0.1,
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [{ role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: instruction }]
|
||||
})
|
||||
});
|
||||
if (!modelRes.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase AI Bot**: Model request failed (' + modelRes.statusCode + ').\n```\n' + modelRes.body.slice(0, 500) + '\n```');
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
const content = JSON.parse(modelRes.body).choices[0].message.content;
|
||||
const cleaned = content.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim();
|
||||
parsed = JSON.parse(cleaned);
|
||||
} catch (e) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase AI Bot**: Could not parse the model response. Please rephrase.');
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 6. Validate ────────────────────────────────────────────────────
|
||||
const knownSlugs = new Set(scripts.map(function (s) { return s.slug; }));
|
||||
const problems = [];
|
||||
if (parsed.clarification) problems.push(parsed.clarification);
|
||||
if (!parsed.slug || !knownSlugs.has(parsed.slug)) problems.push('I could not match the request to a known script.');
|
||||
const sanitized = sanitizeOperations(parsed.operations);
|
||||
const validOps = sanitized.validOps;
|
||||
problems.push.apply(problems, sanitized.problems);
|
||||
if (validOps.length === 0) problems.push('No concrete, supported change was found.');
|
||||
|
||||
if (problems.length) {
|
||||
await addReaction('confused');
|
||||
await postComment(
|
||||
'🤔 **PocketBase AI Bot**: I need a bit more to act on that.\n\n- ' + problems.join('\n- ') +
|
||||
'\n\nTry naming the script and the exact change, e.g. `@pocketbase-bot set RAM to 4096 on zigbee2mqtt`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 7. Propose (do NOT apply yet) ──────────────────────────────────
|
||||
const action = { slug: parsed.slug, operations: validOps };
|
||||
const bullets = validOps.map(function (op) {
|
||||
if (op.kind === 'field') return '- `' + op.field + '` → `' + JSON.stringify(op.value) + '`';
|
||||
if (op.kind === 'note') return '- note ' + op.action + ' `' + op.type + '`' + (op.text ? ': ' + op.text : '');
|
||||
return '- method ' + op.action + ' `' + op.type + '`: ' + JSON.stringify(op.changes);
|
||||
}).join('\n');
|
||||
const marker = '<!-- pocketbase-pending: ' + Buffer.from(JSON.stringify(action), 'utf8').toString('base64') + ' -->';
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'🤖 **PocketBase AI Bot** — please confirm\n\n' +
|
||||
(parsed.human_summary ? '> ' + parsed.human_summary + '\n\n' : '') +
|
||||
'**Target:** `' + action.slug + '`\n**Proposed changes:**\n' + bullets + '\n\n' +
|
||||
'Reply **`@pocketbase-bot confirm`** to apply, or restate the request to adjust.\n' + marker
|
||||
);
|
||||
})().catch(function (e) {
|
||||
console.error('Fatal error:', e && (e.message || e));
|
||||
process.exit(1);
|
||||
});
|
||||
ENDSCRIPT
|
||||
+87
-139
@@ -107,6 +107,9 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -120,7 +123,7 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
|
||||
<details>
|
||||
<summary><h4>September (19 entries)</h4></summary>
|
||||
<summary><h4>September (26 entries)</h4></summary>
|
||||
|
||||
[View September 2026 Changelog](.github/changelogs/2026/09.md)
|
||||
|
||||
@@ -546,17 +549,97 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-09-25
|
||||
## 2026-09-29
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Anki Sync Server ([#17416](https://github.com/community-scripts/ProxmoxVE/pull/17416))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(romm): snapshot Redis hourly like the upstream image [@DenislavDenev](https://github.com/DenislavDenev) ([#17562](https://github.com/community-scripts/ProxmoxVE/pull/17562))
|
||||
- checkmate: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17554](https://github.com/community-scripts/ProxmoxVE/pull/17554))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Generate app headers [@github-actions[bot]](https://github.com/github-actions[bot]) ([core#77](https://github.com/community-scripts/core/pull/77))
|
||||
- Default the scripts base to ProxmoxVE [@MickLesk](https://github.com/MickLesk) ([core#76](https://github.com/community-scripts/core/pull/76))
|
||||
- Generate app headers [@github-actions[bot]](https://github.com/github-actions[bot]) ([core#75](https://github.com/community-scripts/core/pull/75))
|
||||
|
||||
## 2026-09-28
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- Immich: Pin version to 3.2.4 [@vhsdream](https://github.com/vhsdream) ([#17564](https://github.com/community-scripts/ProxmoxVE/pull/17564))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(autocaliweb): wire .env into all service units, not just autocali… [@bobartlett](https://github.com/bobartlett) ([#17561](https://github.com/community-scripts/ProxmoxVE/pull/17561))
|
||||
- Zipline: Change cp command [@Mraedis](https://github.com/Mraedis) ([#17559](https://github.com/community-scripts/ProxmoxVE/pull/17559))
|
||||
- Borg-UI: start through upstream's start.sh so migrations run [@MickLesk](https://github.com/MickLesk) ([#17563](https://github.com/community-scripts/ProxmoxVE/pull/17563))
|
||||
- linkding: serve favicons and preview images [@MickLesk](https://github.com/MickLesk) ([#17557](https://github.com/community-scripts/ProxmoxVE/pull/17557))
|
||||
- SparkyFitness: Fill in the nginx resolver 1.7.3 added [@MickLesk](https://github.com/MickLesk) ([#17556](https://github.com/community-scripts/ProxmoxVE/pull/17556))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Plane: Replace MinIO installation with Silo [@MickLesk](https://github.com/MickLesk) ([#17347](https://github.com/community-scripts/ProxmoxVE/pull/17347))
|
||||
|
||||
## 2026-09-27
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(aurral): bump to Node 26 and bypass strict engine pin [@JodyVanden](https://github.com/JodyVanden) ([#17543](https://github.com/community-scripts/ProxmoxVE/pull/17543))
|
||||
- Tracearr: fetch map tiles on install/update [@durzo](https://github.com/durzo) ([#17544](https://github.com/community-scripts/ProxmoxVE/pull/17544))
|
||||
- fix(autocaliweb): define INSTALL_DIR before first use [@bobartlett](https://github.com/bobartlett) ([#17545](https://github.com/community-scripts/ProxmoxVE/pull/17545))
|
||||
|
||||
## 2026-09-26
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Manyfold: make /opt/manyfold_data writable for the manyfold user [@sergstepanenko](https://github.com/sergstepanenko) ([#17534](https://github.com/community-scripts/ProxmoxVE/pull/17534))
|
||||
- steamcmd: update itself before installing Satisfactory [@MickLesk](https://github.com/MickLesk) ([#17526](https://github.com/community-scripts/ProxmoxVE/pull/17526))
|
||||
- AudioMuse-AI: build noavx2 environment on Python 3.11 [@MickLesk](https://github.com/MickLesk) ([#17528](https://github.com/community-scripts/ProxmoxVE/pull/17528))
|
||||
- LobeHub: pin lobehub/ui at 5.49 while LobeHub's stable release needs it [@MickLesk](https://github.com/MickLesk) ([#17531](https://github.com/community-scripts/ProxmoxVE/pull/17531))
|
||||
- Keep Trilium updates on the server releases [@MickLesk](https://github.com/MickLesk) ([#17525](https://github.com/community-scripts/ProxmoxVE/pull/17525))
|
||||
- NginxProxyManager: generate admin config and repair certbot's venv on update [@MickLesk](https://github.com/MickLesk) ([#17523](https://github.com/community-scripts/ProxmoxVE/pull/17523))
|
||||
- Komodo: add KOMODO_HOST | fix broken spinner [@MickLesk](https://github.com/MickLesk) ([#17481](https://github.com/community-scripts/ProxmoxVE/pull/17481))
|
||||
- romm: run rq cron and a scans worker for RomM 5.3 [@davidbb](https://github.com/davidbb) ([#17502](https://github.com/community-scripts/ProxmoxVE/pull/17502))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Semaphore: fix BoltDB migration by pinning 2.18.30 [@MickLesk](https://github.com/MickLesk) ([#17439](https://github.com/community-scripts/ProxmoxVE/pull/17439))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep a dash-led bridge comment out of whiptail's options [@MickLesk](https://github.com/MickLesk) ([core#74](https://github.com/community-scripts/core/pull/74))
|
||||
|
||||
## 2026-09-25
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- RustFS ([#17499](https://github.com/community-scripts/ProxmoxVE/pull/17499))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Ignis: take the Obsidian version from release tag [@MickLesk](https://github.com/MickLesk) ([#17503](https://github.com/community-scripts/ProxmoxVE/pull/17503))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Umbrel OS VM [@MickLesk](https://github.com/MickLesk) ([#17505](https://github.com/community-scripts/ProxmoxVE/pull/17505))
|
||||
- Docker-LXC: remove Portainer installation from install [@MickLesk](https://github.com/MickLesk) ([#17493](https://github.com/community-scripts/ProxmoxVE/pull/17493))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- incus: do not let an install script block on an invisible prompt [@MickLesk](https://github.com/MickLesk) ([core#71](https://github.com/community-scripts/core/pull/71))
|
||||
- Incus: make prompts visible for end-user [@MickLesk](https://github.com/MickLesk) ([core#73](https://github.com/community-scripts/core/pull/73))
|
||||
- incus: do not let an install script block on an invisible prompt [@MickLesk](https://github.com/MickLesk) ([core#71](https://github.com/community-scripts/core/pull/71))
|
||||
- tel: report progress from installs only, not updates [@MickLesk](https://github.com/MickLesk) ([core#72](https://github.com/community-scripts/core/pull/72))
|
||||
|
||||
## 2026-09-24
|
||||
@@ -1178,139 +1261,4 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Calibre-Web database creation [@MickLesk](https://github.com/MickLesk) ([#16842](https://github.com/community-scripts/ProxmoxVE/pull/16842))
|
||||
- mastodon: read .ruby-version dynamically instead of hardcoding 4.0.5 [@MickLesk](https://github.com/MickLesk) ([#16829](https://github.com/community-scripts/ProxmoxVE/pull/16829))
|
||||
|
||||
## 2026-08-27
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Seanime ([#16777](https://github.com/community-scripts/ProxmoxVE/pull/16777))
|
||||
- Yopass ([#16778](https://github.com/community-scripts/ProxmoxVE/pull/16778))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- excalidash: Add a sed to switch to the correct DB Provider [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#16814](https://github.com/community-scripts/ProxmoxVE/pull/16814))
|
||||
- fix(stirling-pdf): remove broken ExecStop using %n [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#16813](https://github.com/community-scripts/ProxmoxVE/pull/16813))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fix: Incorporate the new update functions to prevent a empty grep on … [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#16809](https://github.com/community-scripts/ProxmoxVE/pull/16809))
|
||||
|
||||
## 2026-08-26
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- CentOS ([#16772](https://github.com/community-scripts/ProxmoxVE/pull/16772))
|
||||
- AlmaLinux ([#16771](https://github.com/community-scripts/ProxmoxVE/pull/16771))
|
||||
- Fedora ([#16770](https://github.com/community-scripts/ProxmoxVE/pull/16770))
|
||||
- Devuan ([#16773](https://github.com/community-scripts/ProxmoxVE/pull/16773))
|
||||
- OpenEuler ([#16774](https://github.com/community-scripts/ProxmoxVE/pull/16774))
|
||||
- Gentoo ([#16775](https://github.com/community-scripts/ProxmoxVE/pull/16775))
|
||||
- openSUSE ([#16776](https://github.com/community-scripts/ProxmoxVE/pull/16776))
|
||||
- Directus ([#16779](https://github.com/community-scripts/ProxmoxVE/pull/16779))
|
||||
- HAProxy ([#16760](https://github.com/community-scripts/ProxmoxVE/pull/16760))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- [Fix]: LimeSurvey - enable Apache mod_rewrite [@jonathan8devs](https://github.com/jonathan8devs) ([#16767](https://github.com/community-scripts/ProxmoxVE/pull/16767))
|
||||
- endurain: migrate legacy FRONTEND_DIR path on update [@MickLesk](https://github.com/MickLesk) ([#16794](https://github.com/community-scripts/ProxmoxVE/pull/16794))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- tools.func: recognize bare XZ-compressed tarballs in fetch_and_deploy* [@MickLesk](https://github.com/MickLesk) ([#16796](https://github.com/community-scripts/ProxmoxVE/pull/16796))
|
||||
- tools.func: fix mongodb version comparison, guard apt purge against removing dependents [@MickLesk](https://github.com/MickLesk) ([#16795](https://github.com/community-scripts/ProxmoxVE/pull/16795))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- github: teach the PocketBase bot every field [@MickLesk](https://github.com/MickLesk) ([#16781](https://github.com/community-scripts/ProxmoxVE/pull/16781))
|
||||
|
||||
## 2026-08-25
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Budget-Board ([#16714](https://github.com/community-scripts/ProxmoxVE/pull/16714))
|
||||
- Maintainerr ([#16716](https://github.com/community-scripts/ProxmoxVE/pull/16716))
|
||||
- pyLoad ([#16717](https://github.com/community-scripts/ProxmoxVE/pull/16717))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(apache-tika): handle upstream's switch from jar to zip distribution [@Munza2020](https://github.com/Munza2020) ([#16726](https://github.com/community-scripts/ProxmoxVE/pull/16726))
|
||||
- Fix Firecrawl install: add ccache dependency for koffi native build [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#16746](https://github.com/community-scripts/ProxmoxVE/pull/16746))
|
||||
- excalidash: Fix broken Update [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#16747](https://github.com/community-scripts/ProxmoxVE/pull/16747))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Migrate the (remaining) Top 25 scripts to the new core engine [@MickLesk](https://github.com/MickLesk) ([#16749](https://github.com/community-scripts/ProxmoxVE/pull/16749))
|
||||
|
||||
## 2026-08-24
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- ArchLinux ([#16715](https://github.com/community-scripts/ProxmoxVE/pull/16715))
|
||||
- AirTrail ([#16713](https://github.com/community-scripts/ProxmoxVE/pull/16713))
|
||||
- RockyLinux ([#16718](https://github.com/community-scripts/ProxmoxVE/pull/16718))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Add setup_mongodb back in Omada [@lucacome](https://github.com/lucacome) ([#16736](https://github.com/community-scripts/ProxmoxVE/pull/16736))
|
||||
- fireshare: source fireshare.env during update [@MickLesk](https://github.com/MickLesk) ([#16706](https://github.com/community-scripts/ProxmoxVE/pull/16706))
|
||||
- netbox: serve on plain HTTP too, port 80 forced HTTPS redirect broke reverse proxies [@MickLesk](https://github.com/MickLesk) ([#16707](https://github.com/community-scripts/ProxmoxVE/pull/16707))
|
||||
- FileFlows: Fix Download URL [@MickLesk](https://github.com/MickLesk) ([#16708](https://github.com/community-scripts/ProxmoxVE/pull/16708))
|
||||
- Gitea: fix git-over-SSH auth, group-writable home dir tripped sshd StrictModes [@MickLesk](https://github.com/MickLesk) ([#16710](https://github.com/community-scripts/ProxmoxVE/pull/16710))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- post-pve/pbs-install: fix component_exists_in_sources matching substrings of hyphenated tokens [@MickLesk](https://github.com/MickLesk) ([#16709](https://github.com/community-scripts/ProxmoxVE/pull/16709))
|
||||
|
||||
## 2026-08-23
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Set default ProxmoxVE raw URL in PVE-UPS & fix var_cpu sorting [@MickLesk](https://github.com/MickLesk) ([#16689](https://github.com/community-scripts/ProxmoxVE/pull/16689))
|
||||
|
||||
## 2026-08-22
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- pve-ups ([#16670](https://github.com/community-scripts/ProxmoxVE/pull/16670))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- docuseal: use DocuSeal's patched PDFium build to fix service start [@MickLesk](https://github.com/MickLesk) ([#16673](https://github.com/community-scripts/ProxmoxVE/pull/16673))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- update authentik to 2026.8.0 [@thieneret](https://github.com/thieneret) ([#16674](https://github.com/community-scripts/ProxmoxVE/pull/16674))
|
||||
|
||||
## 2026-08-21
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- wallos: fix: migrations [@CrazyWolf13](https://github.com/CrazyWolf13) ([#16666](https://github.com/community-scripts/ProxmoxVE/pull/16666))
|
||||
- barassistant: fix: compatibility with v6 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#16665](https://github.com/community-scripts/ProxmoxVE/pull/16665))
|
||||
- Immichframe: remove settings.yaml UUID placeholder [@MickLesk](https://github.com/MickLesk) ([#16660](https://github.com/community-scripts/ProxmoxVE/pull/16660))
|
||||
- openziti-controller: redirect stdin from /dev/null to skip postinst's interactive bootstrap prompt [@MickLesk](https://github.com/MickLesk) ([#16650](https://github.com/community-scripts/ProxmoxVE/pull/16650))
|
||||
- bookorbit: bump default RAM to prevent tsc OOM segfault during nest build [@MickLesk](https://github.com/MickLesk) ([#16649](https://github.com/community-scripts/ProxmoxVE/pull/16649))
|
||||
- immich: split jpegli into its own build step, resolve library revisions dynamically [@MickLesk](https://github.com/MickLesk) ([#16656](https://github.com/community-scripts/ProxmoxVE/pull/16656))
|
||||
- tdarr: make unzip non-interactive to prevent hang [@MickLesk](https://github.com/MickLesk) ([#16648](https://github.com/community-scripts/ProxmoxVE/pull/16648))
|
||||
- immich: fix jpegli patch path after upstream base-images split jpegli from libjxl [@MickLesk](https://github.com/MickLesk) ([#16647](https://github.com/community-scripts/ProxmoxVE/pull/16647))
|
||||
- mastodon: read .ruby-version dynamically instead of hardcoding 4.0.5 [@MickLesk](https://github.com/MickLesk) ([#16829](https://github.com/community-scripts/ProxmoxVE/pull/16829))
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://docs.ankiweb.net/sync-server.html
|
||||
|
||||
APP="Anki-Sync-Server"
|
||||
var_tags="${var_tags:-anki;flashcards;sync}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -f /opt/anki-sync-server/.env ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "anki-sync-server" "ankitects/anki"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop anki-sync-server
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/anki-sync-server/.env /opt/anki-sync-server/data
|
||||
|
||||
msg_info "Updating Anki Sync Server"
|
||||
$STD uv pip install --python /opt/anki-sync-server/venv/bin/python "anki==$(get_latest_github_release "ankitects/anki")"
|
||||
cat <<EOF >~/.anki-sync-server
|
||||
$(get_latest_github_release "ankitects/anki")
|
||||
EOF
|
||||
msg_ok "Updated Anki Sync Server"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start anki-sync-server
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8080${CL}"
|
||||
+3
-1
@@ -41,6 +41,7 @@ function update_script() {
|
||||
|
||||
AUDIOMUSE_BACKEND="$(cat /opt/audiomuse-ai_data/.backend 2>/dev/null || echo cpu)"
|
||||
REQ_COMMON="common.txt"
|
||||
PY_VERSION="3.12"
|
||||
case "$AUDIOMUSE_BACKEND" in
|
||||
gpu)
|
||||
REQ_ACCEL="gpu.txt"
|
||||
@@ -49,6 +50,7 @@ function update_script() {
|
||||
cpu-noavx2)
|
||||
REQ_COMMON="common-noavx2.txt"
|
||||
REQ_ACCEL="cpu-noavx2.txt"
|
||||
PY_VERSION="3.11"
|
||||
;;
|
||||
*)
|
||||
REQ_ACCEL="cpu.txt"
|
||||
@@ -57,7 +59,7 @@ function update_script() {
|
||||
|
||||
msg_info "Updating Python Environment (${AUDIOMUSE_BACKEND})"
|
||||
cd /opt/audiomuse-ai
|
||||
$STD uv venv --seed --python 3.12 /opt/audiomuse-ai/.venv
|
||||
$STD uv venv --seed --python "$PY_VERSION" /opt/audiomuse-ai/.venv
|
||||
$STD uv pip install --python /opt/audiomuse-ai/.venv \
|
||||
-r "/opt/audiomuse-ai/requirements/${REQ_COMMON}" \
|
||||
-r "/opt/audiomuse-ai/requirements/${REQ_ACCEL}"
|
||||
|
||||
+4
-1
@@ -38,13 +38,16 @@ function update_script() {
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "aurral" "lklynet/aurral" "tarball"
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
|
||||
msg_info "Updating Aurral"
|
||||
cd /opt/aurral
|
||||
export VITE_APP_VERSION="$(cat ~/.aurral)"
|
||||
export VITE_GITHUB_REPO="lklynet/aurral"
|
||||
export VITE_RELEASE_CHANNEL="stable"
|
||||
# aurral's .npmrc sets engine-strict=true and pins an exact node patch (e.g. 26.8.x),
|
||||
# which NodeSource can't match since it only ships the latest patch per major.
|
||||
export npm_config_engine_strict=false
|
||||
$STD npm ci --workspace frontend --include-workspace-root=false
|
||||
$STD npm run build --workspace frontend
|
||||
$STD npm ci --workspace backend --omit=dev --include=optional --include-workspace-root=false
|
||||
|
||||
+31
-9
@@ -32,15 +32,9 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if grep -q -- '--workers 2' /etc/systemd/system/borg-ui.service 2>/dev/null; then
|
||||
msg_info "Reducing Service to a single worker"
|
||||
sed -i 's/--workers 2/--workers 1/' /etc/systemd/system/borg-ui.service
|
||||
systemctl daemon-reload
|
||||
systemctl try-restart borg-ui
|
||||
msg_ok "Reduced Service to a single worker"
|
||||
fi
|
||||
|
||||
local changed=0
|
||||
if check_for_gh_release "borg-ui" "karanhudia/borg-ui"; then
|
||||
changed=1
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop borg-ui
|
||||
msg_ok "Stopped Service"
|
||||
@@ -110,9 +104,37 @@ function update_script() {
|
||||
$STD uv venv --python "$BORG_PYTHON" /opt/borg-ui/.venv
|
||||
$STD uv pip install --python /opt/borg-ui/.venv -r requirements.txt
|
||||
msg_ok "Updated Python Environment"
|
||||
fi
|
||||
|
||||
if [[ -f /opt/borg-ui/packaging/native/start.sh ]] && ! grep -q 'packaging/native/start.sh' /etc/systemd/system/borg-ui.service; then
|
||||
msg_info "Switching Service to the upstream start script"
|
||||
cat <<EOF >/etc/systemd/system/borg-ui.service
|
||||
[Unit]
|
||||
Description=Borg-UI
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/borg-ui
|
||||
EnvironmentFile=/opt/borg-ui/.env
|
||||
Environment=BORG_UI_VENV=/opt/borg-ui/.venv
|
||||
ExecStart=/bin/bash /opt/borg-ui/packaging/native/start.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
msg_ok "Switched Service to the upstream start script"
|
||||
changed=1
|
||||
fi
|
||||
|
||||
if [[ "$changed" == 1 ]]; then
|
||||
msg_info "Starting Service"
|
||||
systemctl start borg-ui
|
||||
systemctl restart borg-ui
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
___ __ _ _____ _____
|
||||
/ | ____ / /__(_) / ___/__ ______ _____ / ___/___ ______ _____ _____
|
||||
/ /| | / __ \/ //_/ /_____\__ \/ / / / __ \/ ___/_____\__ \/ _ \/ ___/ | / / _ \/ ___/
|
||||
/ ___ |/ / / / ,< / /_____/__/ / /_/ / / / / /__/_____/__/ / __/ / | |/ / __/ /
|
||||
/_/ |_/_/ /_/_/|_/_/ /____/\__, /_/ /_/\___/ /____/\___/_/ |___/\___/_/
|
||||
/____/
|
||||
@@ -0,0 +1,6 @@
|
||||
____ __ ___________
|
||||
/ __ \__ _______/ /_/ ____/ ___/
|
||||
/ /_/ / / / / ___/ __/ /_ \__ \
|
||||
/ _, _/ /_/ (__ ) /_/ __/ ___/ /
|
||||
/_/ |_|\__,_/____/\__/_/ /____/
|
||||
|
||||
+8
-3
@@ -49,9 +49,14 @@ function update_script() {
|
||||
msg_ok "Built Ignis"
|
||||
|
||||
msg_info "Checking Obsidian Web Assets"
|
||||
OBSIDIAN_VERSION=$(grep -oP 'OBSIDIAN_VERSION=\K[0-9.]+' /opt/ignis/apps/ignis-server/Dockerfile | head -n1)
|
||||
[[ -z "$OBSIDIAN_VERSION" ]] && OBSIDIAN_VERSION="$(cat /opt/ignis_data/obsidian.version 2>/dev/null)"
|
||||
if [[ -n "$OBSIDIAN_VERSION" && "$OBSIDIAN_VERSION" != "$(cat /opt/ignis_data/obsidian.version 2>/dev/null)" ]]; then
|
||||
# From the release tag (0.8.13+obsidian.1.13.7): upstream renamed the Dockerfile variable.
|
||||
OBSIDIAN_VERSION=""
|
||||
[[ "${IGNIS_BUILD:-}" == *obsidian.* ]] && OBSIDIAN_VERSION="${IGNIS_BUILD##*obsidian.}"
|
||||
if [[ ! "$OBSIDIAN_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then
|
||||
OBSIDIAN_VERSION="$(grep -ohP '(OBSIDIAN_VERSION|IGNIS_OBSIDIAN_PIN)=\K[0-9.]+' /opt/ignis/apps/ignis-server/Dockerfile 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
[[ "$OBSIDIAN_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]] || msg_warn "Could not determine the paired Obsidian version - keeping the installed assets"
|
||||
if [[ "$OBSIDIAN_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ && "$OBSIDIAN_VERSION" != "$(cat /opt/ignis_data/obsidian.version 2>/dev/null)" ]]; then
|
||||
rm -rf /opt/ignis_data/obsidian-app
|
||||
mkdir -p /opt/ignis_data/obsidian-app
|
||||
curl -fsSL -o /tmp/obsidian.asar.gz "https://github.com/obsidianmd/obsidian-releases/releases/download/v${OBSIDIAN_VERSION}/obsidian-${OBSIDIAN_VERSION}.asar.gz"
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ EOF
|
||||
msg_ok "Image-processing libraries up to date"
|
||||
fi
|
||||
|
||||
RELEASE="v3.2.2"
|
||||
RELEASE="v3.2.4"
|
||||
if check_for_gh_release "Immich" "immich-app/immich" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
|
||||
if [[ $(cat ~/.immich) > "2.5.1" ]]; then
|
||||
msg_info "Enabling Maintenance Mode"
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ function update_script() {
|
||||
|
||||
msg_warn "⚠️ ${APP} has been migrated to an addon script."
|
||||
echo ""
|
||||
msg_info "This is a one-time migration. After this, you can update ${APP} anytime with:"
|
||||
msg_custom "ℹ️" "${YW}" "This is a one-time migration. After this, you can update ${APP} anytime with:"
|
||||
echo -e "${TAB}${TAB}${GN}update_komodo${CL} or ${GN}bash <(curl -fsSL ${ADDON_SCRIPT})${CL}"
|
||||
echo ""
|
||||
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
|
||||
|
||||
@@ -32,6 +32,16 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if grep -q 'alias /opt/linkding/static/;' /etc/nginx/sites-available/linkding 2>/dev/null; then
|
||||
msg_info "Serving Favicons and Preview Images"
|
||||
sed -i \
|
||||
-e 's|location /static/ {|location ~ ^/static/(.*)$ {|' \
|
||||
-e 's|alias /opt/linkding/static/;|root /opt/linkding;\n try_files /static/$1 /data/favicons/$1 /data/previews/$1 =404;\n add_header Content-Security-Policy "sandbox";|' \
|
||||
/etc/nginx/sites-available/linkding
|
||||
$STD systemctl reload nginx
|
||||
msg_ok "Serving Favicons and Preview Images"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "linkding" "sissbruecker/linkding"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop nginx linkding linkding-tasks
|
||||
|
||||
@@ -46,6 +46,9 @@ function update_script() {
|
||||
msg_info "Building Application"
|
||||
cd /opt/lobehub
|
||||
export NODE_OPTIONS="--max-old-space-size=8192"
|
||||
if grep -qE '"@lobehub/ui": "\^5\.4[0-9]\.' package.json; then
|
||||
sed -i "/^overrides:/a\ '@lobehub/ui': ~5.49.1" pnpm-workspace.yaml
|
||||
fi
|
||||
$STD pnpm install
|
||||
$STD pnpm run build:docker
|
||||
unset NODE_OPTIONS
|
||||
|
||||
@@ -118,10 +118,18 @@ EOF
|
||||
$STD "$CERTBOT_PYTHON" -m ensurepip --upgrade
|
||||
fi
|
||||
$STD "$CERTBOT_PYTHON" -m pip install --upgrade pip setuptools wheel
|
||||
find /opt/certbot/lib/python3*/site-packages -maxdepth 1 -name '*.dist-info' -type d ! -exec test -e '{}/RECORD' ';' -exec rm -rf '{}' + 2>/dev/null || true
|
||||
$STD "$CERTBOT_PYTHON" -m pip install --upgrade certbot certbot-dns-cloudflare
|
||||
msg_ok "Updated Certbot"
|
||||
fi
|
||||
|
||||
if [[ -f /etc/nginx/conf.d/production.conf.template && ! -f /etc/nginx/conf.d/production.conf ]]; then
|
||||
msg_info "Restoring Admin Interface"
|
||||
sed 's/{{NPM_ADMIN_PORT}}/81/g' /etc/nginx/conf.d/production.conf.template >/etc/nginx/conf.d/production.conf
|
||||
systemctl restart openresty
|
||||
msg_ok "Restored Admin Interface"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "nginxproxymanager" "NginxProxyManager/nginx-proxy-manager"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop openresty
|
||||
@@ -159,6 +167,10 @@ EOF
|
||||
cp /opt/nginxproxymanager/docker/rootfs/etc/logrotate.d/nginx-proxy-manager /etc/logrotate.d/nginx-proxy-manager
|
||||
ln -sf /etc/nginx/nginx.conf /etc/nginx/conf/nginx.conf
|
||||
rm -f /etc/nginx/conf.d/dev.conf
|
||||
if [[ -f /etc/nginx/conf.d/production.conf.template ]]; then
|
||||
ADMIN_PORT=$(grep -oP '^\s*listen\s+\K[0-9]+(?=\s+default)' /etc/nginx/conf.d/production.conf 2>/dev/null | head -n1 || true)
|
||||
sed "s/{{NPM_ADMIN_PORT}}/${ADMIN_PORT:-81}/g" /etc/nginx/conf.d/production.conf.template >/etc/nginx/conf.d/production.conf
|
||||
fi
|
||||
|
||||
mkdir -p /tmp/nginx/body \
|
||||
/run/nginx \
|
||||
|
||||
+26
-3
@@ -40,9 +40,32 @@ function update_script() {
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
# RomM 5.3.0 replaced rq-scheduler with `rq cron` and moved scans to their own queue
|
||||
if grep -q rqscheduler /etc/systemd/system/romm-scheduler.service 2>/dev/null; then
|
||||
msg_info "Migrating RQ services"
|
||||
sed -i -e '/RQ_REDIS_/d' \
|
||||
-e 's|^ExecStart=.*|ExecStart=/opt/romm/.venv/bin/rq cron --path /opt/romm/backend --url redis://127.0.0.1:6379/0 tasks.cron_config|' \
|
||||
/etc/systemd/system/romm-scheduler.service
|
||||
sed -i 's|bin/rq worker --path|bin/rq worker --with-scheduler --path|' /etc/systemd/system/romm-worker.service
|
||||
sed -e 's|^Description=.*|Description=RomM RQ Scan Worker|' \
|
||||
-e 's|^ExecStart=.*|ExecStart=/opt/romm/.venv/bin/rq worker --with-scheduler --path /opt/romm/backend --url redis://127.0.0.1:6379/0 scans|' \
|
||||
/etc/systemd/system/romm-worker.service >/etc/systemd/system/romm-scan-worker.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable -q --now romm-scan-worker
|
||||
systemctl restart romm-worker romm-scheduler
|
||||
msg_ok "Migrated RQ services"
|
||||
fi
|
||||
|
||||
if ! grep -q '^save ' /etc/redis/redis.conf; then
|
||||
msg_info "Reducing Redis snapshot frequency"
|
||||
echo 'save 3600 1' >>/etc/redis/redis.conf
|
||||
$STD redis-cli CONFIG SET save "3600 1"
|
||||
msg_ok "Reduced Redis snapshot frequency"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "romm" "rommapp/romm"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop romm-backend romm-worker romm-scheduler romm-watcher
|
||||
systemctl stop romm-backend romm-worker romm-scan-worker romm-scheduler romm-watcher
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
create_backup /opt/romm/.env
|
||||
@@ -231,14 +254,14 @@ DROPEOF
|
||||
fi
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start romm-backend romm-worker romm-scheduler romm-watcher
|
||||
systemctl start romm-backend romm-worker romm-scan-worker romm-scheduler romm-watcher
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "EmulatorJS" "EmulatorJS/EmulatorJS" "v4.2.3"; then
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "EmulatorJS" "EmulatorJS/EmulatorJS" "prebuild" "v4.2.3" "/opt/romm/frontend/dist/assets/emulatorjs" "4.2.3.7z"
|
||||
systemctl restart romm-backend romm-worker romm-scheduler romm-watcher
|
||||
systemctl restart romm-backend romm-worker romm-scan-worker romm-scheduler romm-watcher
|
||||
msg_ok "Updated EmulatorJS successfully"
|
||||
fi
|
||||
exit
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/rustfs/rustfs
|
||||
|
||||
APP="RustFS"
|
||||
var_tags="${var_tags:-storage;s3}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-20}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -f /etc/default/rustfs ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "rustfs" "rustfs/rustfs"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop rustfs
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "rustfs" "rustfs/rustfs" "prebuild" "latest" "/opt/rustfs" "rustfs-linux-$(arch_resolve x86_64 aarch64)-gnu-*.zip"
|
||||
chmod +x /opt/rustfs/rustfs
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start rustfs
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Console:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:9001${CL}"
|
||||
echo -e "${INFO}${YW}S3 API on port 9000 - keys are in /etc/default/rustfs${CL}"
|
||||
@@ -39,6 +39,7 @@ function update_script() {
|
||||
create_backup /home/steam/.config/Epic/FactoryGame/Saved
|
||||
|
||||
msg_info "Updating Satisfactory"
|
||||
$STD runuser -u steam -- /opt/steamcmd/steamcmd.sh +login anonymous +quit || true
|
||||
if $STD runuser -u steam -- /opt/steamcmd/steamcmd.sh \
|
||||
+force_install_dir /opt/satisfactory/server \
|
||||
+login anonymous \
|
||||
|
||||
+49
-23
@@ -32,42 +32,68 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "semaphore" "semaphoreui/semaphore"; then
|
||||
if [[ -f /opt/semaphore/semaphore_db.bolt ]]; then
|
||||
msg_warn "WARNING: Due to bugs with BoltDB database, update script will move your application"
|
||||
msg_warn "to use SQLite database instead. Make sure you have a backup of your data!"
|
||||
echo ""
|
||||
read -r -p "${TAB3}Do you want to continue? (y/N): " CONFIRM
|
||||
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
||||
exit 0
|
||||
else
|
||||
msg_info "Moving from BoltDB to SQLite"
|
||||
sed -i \
|
||||
-e 's|"bolt": {|"sqlite": {|' \
|
||||
-e 's|/semaphore_db.bolt"|/database.sqlite"|' \
|
||||
-e '/semaphore_db.bolt/d' \
|
||||
-e '/"dialect"/d' \
|
||||
-e '/^ },$/a\ "dialect": "sqlite",' \
|
||||
/opt/semaphore/config.json
|
||||
msg_ok "Moved from BoltDB to SQLite"
|
||||
fi
|
||||
# Outside the release check on purpose: a container left behind by the old
|
||||
# order already carries the newest version, so the check would skip it.
|
||||
local migrated=0
|
||||
if [[ -f /opt/semaphore/semaphore_db.bolt ]]; then
|
||||
msg_warn "WARNING: Due to bugs with BoltDB database, update script will move your application"
|
||||
msg_warn "to use SQLite database instead. Make sure you have a backup of your data!"
|
||||
echo ""
|
||||
read -r -p "${TAB3}Do you want to continue? (y/N): " CONFIRM
|
||||
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop semaphore
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
fetch_and_deploy_gh_release "semaphore" "semaphoreui/semaphore" "binary" "latest" "/opt/semaphore" "semaphore_*_linux_$(arch_resolve).deb"
|
||||
msg_info "Moving from BoltDB to SQLite"
|
||||
sed -i \
|
||||
-e 's|"bolt": {|"sqlite": {|' \
|
||||
-e 's|/semaphore_db.bolt"|/database.sqlite"|' \
|
||||
-e '/semaphore_db.bolt/d' \
|
||||
-e '/"dialect"/d' \
|
||||
-e '/^ },$/a\ "dialect": "sqlite",' \
|
||||
/opt/semaphore/config.json
|
||||
msg_ok "Moved from BoltDB to SQLite"
|
||||
|
||||
if [[ -f /opt/semaphore/semaphore_db.bolt ]]; then
|
||||
$STD semaphore migrate --from-boltdb /opt/semaphore/semaphore_db.bolt --config /opt/semaphore/config.json
|
||||
rm -f /opt/semaphore/semaphore_db.bolt
|
||||
# 2.19 dropped BoltDB along with the migration flag, so this has to run on
|
||||
# the last release that still has it.
|
||||
fetch_and_deploy_gh_release "semaphore" "semaphoreui/semaphore" "binary" "v2.18.30" "/opt/semaphore" "semaphore_*_linux_$(arch_resolve).deb"
|
||||
|
||||
msg_info "Migrating BoltDB to SQLite"
|
||||
if [[ -f /opt/semaphore/database.sqlite ]]; then
|
||||
mkdir -p /opt/semaphore/sqlite.bak
|
||||
mv /opt/semaphore/database.sqlite* /opt/semaphore/sqlite.bak/
|
||||
fi
|
||||
sed '/"dialect": "sqlite",/a\ "apps": {"ansible": {}, "terraform": {}, "tofu": {}, "terragrunt": {}, "bash": {}, "powershell": {}, "python": {}, "pulumi": {}},' \
|
||||
/opt/semaphore/config.json >/opt/semaphore/migrate.json
|
||||
if ! $STD semaphore migrate --from-boltdb /opt/semaphore/semaphore_db.bolt --config /opt/semaphore/migrate.json; then
|
||||
msg_error "Migration failed - semaphore_db.bolt was kept, run the update again to retry"
|
||||
exit 1
|
||||
fi
|
||||
rm -f /opt/semaphore/migrate.json
|
||||
mv /opt/semaphore/semaphore_db.bolt /opt/semaphore/semaphore_db.bolt.bak
|
||||
msg_ok "Migrated BoltDB to SQLite"
|
||||
migrated=1
|
||||
fi
|
||||
|
||||
if check_for_gh_release "semaphore" "semaphoreui/semaphore"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop semaphore
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
fetch_and_deploy_gh_release "semaphore" "semaphoreui/semaphore" "binary" "latest" "/opt/semaphore" "semaphore_*_linux_$(arch_resolve).deb"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start semaphore
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
elif ((migrated)); then
|
||||
msg_info "Starting Service"
|
||||
systemctl start semaphore
|
||||
msg_ok "Started Service"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
@@ -62,12 +62,14 @@ function update_script() {
|
||||
|
||||
msg_info "Refreshing Nginx Config"
|
||||
FRONTEND_URL=$(grep -oP '^SPARKY_FITNESS_FRONTEND_URL=\K.*' /etc/sparkyfitness/.env)
|
||||
NGINX_RESOLVER=$(awk '$1=="nameserver" {ns=$2; sub(/%.*/, "", ns); printf "%s%s", sep, (ns ~ /:/ ? "[" ns "]" : ns); sep=" "}' /etc/resolv.conf)
|
||||
sed \
|
||||
-e 's|${SPARKY_FITNESS_SERVER_HOST}|127.0.0.1|g' \
|
||||
-e 's|${SPARKY_FITNESS_SERVER_PORT}|3010|g' \
|
||||
-e "s|\${SPARKY_FITNESS_FRONTEND_URL}|${FRONTEND_URL}|g" \
|
||||
-e 's|${NGINX_LISTEN_PORT}|80|g' \
|
||||
-e 's|${NGINX_RATE_LIMIT}|5r/s|g' \
|
||||
-e "s|\${NGINX_RESOLVER}|${NGINX_RESOLVER:-127.0.0.1}|g" \
|
||||
-e 's|${NGINX_ACCESS_LOG}|/var/log/nginx/sparkyfitness.access.log|g' \
|
||||
-e 's|${NGINX_ERROR_LOG}|/var/log/nginx/sparkyfitness.error.log|g' \
|
||||
-e 's|root /usr/share/nginx/html;|root /var/www/sparkyfitness;|g' \
|
||||
|
||||
@@ -123,6 +123,7 @@ EOF
|
||||
$STD pnpm install --frozen-lockfile --force
|
||||
$STD pnpm turbo telemetry disable
|
||||
$STD pnpm turbo run build --no-daemon --filter=@tracearr/shared --filter=@tracearr/server --filter=@tracearr/web
|
||||
$STD ./scripts/fetch-basemap.sh
|
||||
rm -rf /opt/tracearr
|
||||
mkdir -p /opt/tracearr/{packages/shared,packages/emails,apps/server,apps/web,apps/server/src/db}
|
||||
cp -rf package.json /opt/tracearr/
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ function update_script() {
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
if check_for_gh_release "Trilium" "TriliumNext/Trilium"; then
|
||||
if check_for_gh_release "Trilium" "TriliumNext/Trilium" "" "" "v"; then
|
||||
if [[ -d /opt/trilium/db ]]; then
|
||||
DB_PATH="/opt/trilium/db"
|
||||
DB_RESTORE_PATH="/opt/trilium/db"
|
||||
@@ -53,7 +53,7 @@ function update_script() {
|
||||
rm -rf /opt/trilium
|
||||
msg_ok "Backed up Database"
|
||||
|
||||
fetch_and_deploy_gh_release "Trilium" "TriliumNext/Trilium" "prebuild" "latest" "/opt/trilium" "TriliumNotes-Server-*linux-$(arch_resolve "x64" "arm64").tar.xz"
|
||||
fetch_and_deploy_gh_release "Trilium" "TriliumNext/Trilium" "prebuild" "latest" "/opt/trilium" "TriliumNotes-Server-*linux-$(arch_resolve "x64" "arm64").tar.xz" "v"
|
||||
|
||||
msg_info "Restoring Database"
|
||||
mkdir -p "$(dirname "${DB_RESTORE_PATH}")"
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ function update_script() {
|
||||
|
||||
mkdir -p /opt/zipline-uploads
|
||||
if [ -d /opt/zipline/uploads ] && [ "$(ls -A /opt/zipline/uploads)" ]; then
|
||||
cp -R /opt/zipline/uploads/* /opt/zipline-uploads/
|
||||
cp -R /opt/zipline/uploads/. /opt/zipline-uploads/
|
||||
fi
|
||||
cp /opt/zipline/.env /opt/
|
||||
rm -R /opt/zipline
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://docs.ankiweb.net/sync-server.html
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
UV_PYTHON="3.12" setup_uv
|
||||
|
||||
msg_info "Installing Anki Sync Server"
|
||||
$STD uv venv --python 3.12 /opt/anki-sync-server/venv
|
||||
$STD uv pip install --python /opt/anki-sync-server/venv/bin/python "anki==$(get_latest_github_release "ankitects/anki")"
|
||||
cat <<EOF >~/.anki-sync-server
|
||||
$(get_latest_github_release "ankitects/anki")
|
||||
EOF
|
||||
msg_ok "Installed Anki Sync Server"
|
||||
|
||||
msg_info "Configuring Anki Sync Server"
|
||||
mkdir -p /opt/anki-sync-server/data
|
||||
cat <<EOF >/opt/anki-sync-server/.env
|
||||
SYNC_USER1=anki:$(tr -d '-' </proc/sys/kernel/random/uuid)
|
||||
SYNC_BASE=/opt/anki-sync-server/data
|
||||
SYNC_HOST=0.0.0.0
|
||||
SYNC_PORT=8080
|
||||
EOF
|
||||
chmod 600 /opt/anki-sync-server/.env
|
||||
msg_ok "Configured Anki Sync Server"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/anki-sync-server.service
|
||||
[Unit]
|
||||
Description=Anki Sync Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/anki-sync-server
|
||||
EnvironmentFile=/opt/anki-sync-server/.env
|
||||
ExecStart=/opt/anki-sync-server/venv/bin/python -m anki.syncserver
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now anki-sync-server
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -42,6 +42,7 @@ if [[ "$var_backend" == "auto" ]]; then
|
||||
fi
|
||||
|
||||
REQ_COMMON="common.txt"
|
||||
PY_VERSION="3.12"
|
||||
case "$var_backend" in
|
||||
gpu)
|
||||
REQ_ACCEL="gpu.txt"
|
||||
@@ -53,6 +54,7 @@ cpu)
|
||||
cpu-noavx2)
|
||||
REQ_COMMON="common-noavx2.txt"
|
||||
REQ_ACCEL="cpu-noavx2.txt"
|
||||
PY_VERSION="3.11"
|
||||
;;
|
||||
*)
|
||||
msg_error "Unknown var_backend '${var_backend}' (expected auto, gpu, cpu or cpu-noavx2)"
|
||||
@@ -62,7 +64,7 @@ esac
|
||||
|
||||
msg_info "Setting up Python Environment (${var_backend}, Patience)"
|
||||
cd /opt/audiomuse-ai
|
||||
$STD uv venv --seed --python 3.12 /opt/audiomuse-ai/.venv
|
||||
$STD uv venv --seed --python "$PY_VERSION" /opt/audiomuse-ai/.venv
|
||||
$STD uv pip install --python /opt/audiomuse-ai/.venv \
|
||||
-r "/opt/audiomuse-ai/requirements/${REQ_COMMON}" \
|
||||
-r "/opt/audiomuse-ai/requirements/${REQ_ACCEL}"
|
||||
|
||||
@@ -22,7 +22,7 @@ $STD apt install -y \
|
||||
fonts-dejavu-core
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
|
||||
fetch_and_deploy_gh_release "yt-dlp" "yt-dlp/yt-dlp" "singlefile" "latest" "/usr/local/bin" "yt-dlp"
|
||||
fetch_and_deploy_gh_release "aurral" "lklynet/aurral" "tarball"
|
||||
@@ -32,6 +32,9 @@ cd /opt/aurral
|
||||
export VITE_APP_VERSION="$(cat ~/.aurral)"
|
||||
export VITE_GITHUB_REPO="lklynet/aurral"
|
||||
export VITE_RELEASE_CHANNEL="stable"
|
||||
# aurral's .npmrc sets engine-strict=true and pins an exact node patch (e.g. 26.8.x),
|
||||
# which NodeSource can't match since it only ships the latest patch per major.
|
||||
export npm_config_engine_strict=false
|
||||
$STD npm ci --workspace frontend --include-workspace-root=false
|
||||
$STD npm run build --workspace frontend
|
||||
$STD npm ci --workspace backend --omit=dev --include=optional --include-workspace-root=false
|
||||
|
||||
@@ -54,11 +54,11 @@ $STD /opt/calibre/calibre_postinstall
|
||||
CALIBRE_VERSION=$(cat ~/.calibre)
|
||||
msg_ok "Installed Calibre"
|
||||
|
||||
INSTALL_DIR="/opt/autocaliweb"
|
||||
fetch_and_deploy_codeberg_release "autocaliweb" "gelbphoenix/autocaliweb" "tarball" "latest" "/opt/autocaliweb"
|
||||
setup_uv $INSTALL_DIR
|
||||
|
||||
msg_info "Configuring Autocaliweb"
|
||||
INSTALL_DIR="/opt/autocaliweb"
|
||||
CONFIG_DIR="/etc/autocaliweb"
|
||||
CALIBRE_LIB_DIR="/opt/calibre-library"
|
||||
INGEST_DIR="/opt/acw-book-ingest"
|
||||
@@ -266,6 +266,7 @@ Group=${SERVICE_GROUP}
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
Environment=CALIBRE_DBPATH=${CONFIG_DIR}
|
||||
Environment=HOME=${CONFIG_DIR}
|
||||
EnvironmentFile=${INSTALL_DIR}/.env
|
||||
ExecStart=/bin/bash ${SCRIPTS_DIR}/ingest_watcher.sh
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
@@ -285,6 +286,7 @@ User=${SERVICE_USER}
|
||||
Group=${SERVICE_GROUP}
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
Environment=CALIBRE_DBPATH=${CONFIG_DIR}
|
||||
EnvironmentFile=${INSTALL_DIR}/.env
|
||||
ExecStart=${SCRIPTS_DIR}/auto_zipper_wrapper.sh
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
@@ -303,6 +305,7 @@ After=network.target
|
||||
User=${SERVICE_USER}
|
||||
Group=${SERVICE_GROUP}
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
EnvironmentFile=${INSTALL_DIR}/.env
|
||||
ExecStart=/bin/bash ${SCRIPTS_DIR}/metadata_change_detector_wrapper.sh
|
||||
Restart=always
|
||||
StandardOutput=journal
|
||||
|
||||
@@ -111,7 +111,8 @@ Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/borg-ui
|
||||
EnvironmentFile=/opt/borg-ui/.env
|
||||
ExecStart=/opt/borg-ui/.venv/bin/gunicorn app.main:app --bind 0.0.0.0:8081 --workers 1 --worker-class uvicorn.workers.UvicornWorker --timeout 300
|
||||
Environment=BORG_UI_VENV=/opt/borg-ui/.venv
|
||||
ExecStart=/bin/bash /opt/borg-ui/packaging/native/start.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ $STD apt install -y \
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
MONGO_VERSION="8.0" setup_mongodb
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
fetch_and_deploy_gh_release "checkmate" "bluewave-labs/Checkmate" "tarball"
|
||||
|
||||
msg_info "Configuring Checkmate"
|
||||
|
||||
@@ -32,8 +32,16 @@ msg_ok "Built Ignis"
|
||||
|
||||
msg_info "Downloading Obsidian Web Assets"
|
||||
mkdir -p /opt/ignis_data/{obsidian-app,vaults,data}
|
||||
OBSIDIAN_VERSION=$(grep -oP 'OBSIDIAN_VERSION=\K[0-9.]+' /opt/ignis/apps/ignis-server/Dockerfile | head -n1)
|
||||
[[ -z "$OBSIDIAN_VERSION" ]] && OBSIDIAN_VERSION="1.12.7"
|
||||
# From the release tag (0.8.13+obsidian.1.13.7): upstream renamed the Dockerfile variable.
|
||||
OBSIDIAN_VERSION=""
|
||||
[[ "${IGNIS_BUILD:-}" == *obsidian.* ]] && OBSIDIAN_VERSION="${IGNIS_BUILD##*obsidian.}"
|
||||
if [[ ! "$OBSIDIAN_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then
|
||||
OBSIDIAN_VERSION="$(grep -ohP '(OBSIDIAN_VERSION|IGNIS_OBSIDIAN_PIN)=\K[0-9.]+' /opt/ignis/apps/ignis-server/Dockerfile 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
if [[ ! "$OBSIDIAN_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then
|
||||
msg_error "Could not determine which Obsidian version this Ignis build pairs with"
|
||||
exit 1
|
||||
fi
|
||||
curl -fsSL -o /tmp/obsidian.asar.gz "https://github.com/obsidianmd/obsidian-releases/releases/download/v${OBSIDIAN_VERSION}/obsidian-${OBSIDIAN_VERSION}.asar.gz"
|
||||
gunzip -f /tmp/obsidian.asar.gz
|
||||
$STD asar extract /tmp/obsidian.asar /opt/ignis_data/obsidian-app
|
||||
|
||||
@@ -353,7 +353,7 @@ ML_DIR="${APP_DIR}/machine-learning"
|
||||
GEO_DIR="${INSTALL_DIR}/geodata"
|
||||
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
|
||||
|
||||
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v3.2.2" "$SRC_DIR"
|
||||
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v3.2.4" "$SRC_DIR"
|
||||
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
NODE_VERSION="24" NODE_MODULE="corepack" setup_nodejs
|
||||
|
||||
@@ -101,8 +101,10 @@ server {
|
||||
|
||||
client_max_body_size 20M;
|
||||
|
||||
location /static/ {
|
||||
alias /opt/linkding/static/;
|
||||
location ~ ^/static/(.*)$ {
|
||||
root /opt/linkding;
|
||||
try_files /static/$1 /data/favicons/$1 /data/previews/$1 =404;
|
||||
add_header Content-Security-Policy "sandbox";
|
||||
expires 30d;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ export DATABASE_DRIVER="node"
|
||||
export KEY_VAULTS_SECRET="$(openssl rand -base64 32)"
|
||||
export AUTH_SECRET="$(openssl rand -base64 32)"
|
||||
export APP_URL="http://localhost:3210"
|
||||
if grep -qE '"@lobehub/ui": "\^5\.4[0-9]\.' package.json; then
|
||||
sed -i "/^overrides:/a\ '@lobehub/ui': ~5.49.1" pnpm-workspace.yaml
|
||||
fi
|
||||
$STD pnpm install
|
||||
$STD pnpm run build:docker
|
||||
msg_ok "Built Application"
|
||||
|
||||
@@ -80,7 +80,7 @@ $STD mkdir -p /opt/manyfold_data
|
||||
msg_ok "Configured Manyfold"
|
||||
|
||||
msg_info "Installing Manyfold"
|
||||
chown -R manyfold:manyfold {/home/manyfold,/opt/manyfold}
|
||||
chown -R manyfold:manyfold {/home/manyfold,/opt/manyfold,/opt/manyfold_data}
|
||||
chmod +x /opt/manyfold/user_setup.sh
|
||||
|
||||
$STD sudo -u manyfold bash /opt/manyfold/user_setup.sh
|
||||
|
||||
@@ -96,6 +96,9 @@ cp /opt/nginxproxymanager/docker/rootfs/etc/letsencrypt.ini /etc/letsencrypt.ini
|
||||
cp /opt/nginxproxymanager/docker/rootfs/etc/logrotate.d/nginx-proxy-manager /etc/logrotate.d/nginx-proxy-manager
|
||||
ln -sf /etc/nginx/nginx.conf /etc/nginx/conf/nginx.conf
|
||||
rm -f /etc/nginx/conf.d/dev.conf
|
||||
if [[ -f /etc/nginx/conf.d/production.conf.template ]]; then
|
||||
sed 's/{{NPM_ADMIN_PORT}}/81/g' /etc/nginx/conf.d/production.conf.template >/etc/nginx/conf.d/production.conf
|
||||
fi
|
||||
|
||||
mkdir -p /tmp/nginx/body \
|
||||
/run/nginx \
|
||||
|
||||
+12
-13
@@ -43,9 +43,10 @@ $STD rabbitmqctl add_user plane "${RABBITMQ_PASS}"
|
||||
$STD rabbitmqctl set_permissions -p plane plane ".*" ".*" ".*"
|
||||
msg_ok "Configured RabbitMQ"
|
||||
|
||||
msg_info "Installing MinIO"
|
||||
curl -fsSL https://dl.min.io/server/minio/release/linux-$(arch_resolve)/minio -o /usr/local/bin/minio
|
||||
chmod +x /usr/local/bin/minio
|
||||
fetch_and_deploy_gh_release "silo" "pgsty/silo" "prebuild" "latest" "/opt/silo" "silo_*_linux_$(arch_resolve).tar.gz"
|
||||
fetch_and_deploy_gh_release "mcli" "pgsty/mc" "prebuild" "latest" "/opt/mcli" "mcli_*_linux_$(arch_resolve).tar.gz"
|
||||
|
||||
msg_info "Configuring Silo"
|
||||
mkdir -p /opt/minio/data
|
||||
MINIO_ACCESS_KEY=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
|
||||
MINIO_SECRET_KEY=$(openssl rand -base64 36 | tr -dc 'a-zA-Z0-9' | head -c32)
|
||||
@@ -56,13 +57,13 @@ MINIO_VOLUMES="/opt/minio/data"
|
||||
EOF
|
||||
cat <<EOF >/etc/systemd/system/minio.service
|
||||
[Unit]
|
||||
Description=MinIO Object Storage
|
||||
Description=Silo Object Storage
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/default/minio
|
||||
ExecStart=/usr/local/bin/minio server \$MINIO_VOLUMES --console-address ":9090"
|
||||
ExecStart=/opt/silo/silo server \$MINIO_VOLUMES --console-address ":9090"
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -70,7 +71,7 @@ RestartSec=5
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now minio
|
||||
msg_ok "Installed MinIO"
|
||||
msg_ok "Configured Silo"
|
||||
|
||||
fetch_and_deploy_gh_release "plane" "makeplane/plane" "tarball"
|
||||
|
||||
@@ -175,12 +176,10 @@ $STD /opt/plane-venv/bin/python manage.py configure_instance
|
||||
$STD /opt/plane-venv/bin/python manage.py register_instance "${MACHINE_SIG}"
|
||||
msg_ok "Ran Database Migrations"
|
||||
|
||||
msg_info "Creating Services and MinIO Bucket"
|
||||
curl -fsSL https://dl.min.io/client/mc/release/linux-$(arch_resolve)/mc -o /usr/local/bin/mcli
|
||||
chmod +x /usr/local/bin/mcli
|
||||
$STD /usr/local/bin/mcli alias set plane http://localhost:9000 "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"
|
||||
$STD /usr/local/bin/mcli mb plane/uploads --ignore-existing
|
||||
$STD /usr/local/bin/mcli anonymous set download plane/uploads
|
||||
msg_info "Creating Services and Bucket"
|
||||
$STD /opt/mcli/mcli alias set plane http://localhost:9000 "${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}"
|
||||
$STD /opt/mcli/mcli mb plane/uploads --ignore-existing
|
||||
$STD /opt/mcli/mcli anonymous set download plane/uploads
|
||||
|
||||
cat <<EOF >/etc/systemd/system/plane-api.service
|
||||
[Unit]
|
||||
@@ -279,7 +278,7 @@ MinIO Secret Key: ${MINIO_SECRET_KEY}
|
||||
Secret Key: ${SECRET_KEY}
|
||||
Config: /opt/plane/apps/api/.env
|
||||
EOF
|
||||
msg_ok "Created Services and MinIO Bucket"
|
||||
msg_ok "Created Services and Bucket"
|
||||
|
||||
msg_info "Configuring Nginx"
|
||||
cat <<'EOF' >/etc/nginx/sites-available/plane.conf
|
||||
|
||||
+23
-5
@@ -167,6 +167,7 @@ echo "__version__ = \"$(cat ~/.romm)\"" >/opt/romm/backend/__version__.py
|
||||
|
||||
msg_info "Creating environment file"
|
||||
sed -i 's/^supervised no/supervised systemd/' /etc/redis/redis.conf
|
||||
echo 'save 3600 1' >>/etc/redis/redis.conf
|
||||
systemctl restart redis-server
|
||||
systemctl enable -q --now redis-server
|
||||
AUTH_SECRET_KEY=$(openssl rand -hex 32)
|
||||
@@ -381,7 +382,26 @@ Type=simple
|
||||
WorkingDirectory=/opt/romm/backend
|
||||
EnvironmentFile=/opt/romm/.env
|
||||
Environment="PYTHONPATH=/opt/romm/backend"
|
||||
ExecStart=/opt/romm/.venv/bin/rq worker --path /opt/romm/backend --url redis://127.0.0.1:6379/0 high default low
|
||||
ExecStart=/opt/romm/.venv/bin/rq worker --with-scheduler --path /opt/romm/backend --url redis://127.0.0.1:6379/0 high default low
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat <<EOF >/etc/systemd/system/romm-scan-worker.service
|
||||
[Unit]
|
||||
Description=RomM RQ Scan Worker
|
||||
After=network.target mariadb.service redis-server.service romm-backend.service
|
||||
Requires=mariadb.service redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/romm/backend
|
||||
EnvironmentFile=/opt/romm/.env
|
||||
Environment="PYTHONPATH=/opt/romm/backend"
|
||||
ExecStart=/opt/romm/.venv/bin/rq worker --with-scheduler --path /opt/romm/backend --url redis://127.0.0.1:6379/0 scans
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -400,9 +420,7 @@ Type=simple
|
||||
WorkingDirectory=/opt/romm/backend
|
||||
EnvironmentFile=/opt/romm/.env
|
||||
Environment="PYTHONPATH=/opt/romm/backend"
|
||||
Environment="RQ_REDIS_HOST=127.0.0.1"
|
||||
Environment="RQ_REDIS_PORT=6379"
|
||||
ExecStart=/opt/romm/.venv/bin/rqscheduler --path /opt/romm/backend
|
||||
ExecStart=/opt/romm/.venv/bin/rq cron --path /opt/romm/backend --url redis://127.0.0.1:6379/0 tasks.cron_config
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -429,7 +447,7 @@ RestartSec=5
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl enable -q --now romm-backend romm-worker romm-scheduler romm-watcher
|
||||
systemctl enable -q --now romm-backend romm-worker romm-scan-worker romm-scheduler romm-watcher
|
||||
msg_ok "Created Services"
|
||||
|
||||
motd_ssh
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/rustfs/rustfs
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
mkdir -p /opt/rustfs_data/{data,logs}
|
||||
fetch_and_deploy_gh_release "rustfs" "rustfs/rustfs" "prebuild" "latest" "/opt/rustfs" "rustfs-linux-$(arch_resolve x86_64 aarch64)-gnu-*.zip"
|
||||
chmod +x /opt/rustfs/rustfs
|
||||
|
||||
msg_info "Configuring RustFS"
|
||||
RUSTFS_ACCESS_KEY=$(openssl rand -hex 8)
|
||||
RUSTFS_SECRET_KEY=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
||||
cat <<EOF >/etc/default/rustfs
|
||||
RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY}
|
||||
RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY}
|
||||
RUSTFS_VOLUMES=/opt/rustfs_data/data
|
||||
RUSTFS_ADDRESS=:9000
|
||||
RUSTFS_CONSOLE_ADDRESS=:9001
|
||||
RUSTFS_CONSOLE_ENABLE=true
|
||||
RUSTFS_OBS_LOGGER_LEVEL=error
|
||||
RUSTFS_OBS_LOG_DIRECTORY=/opt/rustfs_data/logs/
|
||||
EOF
|
||||
chmod 600 /etc/default/rustfs
|
||||
msg_ok "Configured RustFS"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/rustfs.service
|
||||
[Unit]
|
||||
Description=RustFS Object Storage
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/rustfs_data
|
||||
EnvironmentFile=/etc/default/rustfs
|
||||
ExecStart=/opt/rustfs/rustfs
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
LimitNOFILE=65536
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now rustfs
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -35,6 +35,7 @@ fetch_and_deploy_from_url \
|
||||
chown -R steam:steam /opt/steamcmd
|
||||
|
||||
msg_info "Installing Satisfactory Dedicated Server"
|
||||
$STD runuser -u steam -- /opt/steamcmd/steamcmd.sh +login anonymous +quit || true
|
||||
$STD runuser -u steam -- /opt/steamcmd/steamcmd.sh \
|
||||
+force_install_dir /opt/satisfactory/server \
|
||||
+login anonymous \
|
||||
|
||||
@@ -81,12 +81,14 @@ systemctl enable -q --now sparkyfitness-server
|
||||
msg_ok "Created SparkyFitness Service"
|
||||
|
||||
msg_info "Configuring Nginx"
|
||||
NGINX_RESOLVER=$(awk '$1=="nameserver" {ns=$2; sub(/%.*/, "", ns); printf "%s%s", sep, (ns ~ /:/ ? "[" ns "]" : ns); sep=" "}' /etc/resolv.conf)
|
||||
sed \
|
||||
-e 's|${SPARKY_FITNESS_SERVER_HOST}|127.0.0.1|g' \
|
||||
-e 's|${SPARKY_FITNESS_SERVER_PORT}|3010|g' \
|
||||
-e "s|\${SPARKY_FITNESS_FRONTEND_URL}|http://${LOCAL_IP}:80|g" \
|
||||
-e 's|${NGINX_LISTEN_PORT}|80|g' \
|
||||
-e 's|${NGINX_RATE_LIMIT}|5r/s|g' \
|
||||
-e "s|\${NGINX_RESOLVER}|${NGINX_RESOLVER:-127.0.0.1}|g" \
|
||||
-e 's|${NGINX_ACCESS_LOG}|/var/log/nginx/sparkyfitness.access.log|g' \
|
||||
-e 's|${NGINX_ERROR_LOG}|/var/log/nginx/sparkyfitness.error.log|g' \
|
||||
-e 's|root /usr/share/nginx/html;|root /var/www/sparkyfitness;|g' \
|
||||
|
||||
@@ -67,6 +67,7 @@ cd /opt/tracearr.build
|
||||
$STD pnpm install --frozen-lockfile --force
|
||||
$STD pnpm turbo telemetry disable
|
||||
$STD pnpm turbo run build --no-daemon --filter=@tracearr/shared --filter=@tracearr/server --filter=@tracearr/web
|
||||
$STD ./scripts/fetch-basemap.sh
|
||||
mkdir -p /opt/tracearr/{packages/shared,packages/emails,apps/server,apps/web,apps/server/src/db}
|
||||
cp -rf package.json /opt/tracearr/
|
||||
cp -rf pnpm-workspace.yaml /opt/tracearr/
|
||||
|
||||
@@ -13,7 +13,7 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
fetch_and_deploy_gh_release "Trilium" "TriliumNext/Trilium" "prebuild" "latest" "/opt/trilium" "TriliumNotes-Server-*linux-$(arch_resolve "x64" "arm64").tar.xz"
|
||||
fetch_and_deploy_gh_release "Trilium" "TriliumNext/Trilium" "prebuild" "latest" "/opt/trilium" "TriliumNotes-Server-*linux-$(arch_resolve "x64" "arm64").tar.xz" "v"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/trilium.service
|
||||
|
||||
+12
-2
@@ -199,6 +199,7 @@ function install() {
|
||||
sed -i "s/^KOMODO_INIT_ADMIN_PASSWORD=changeme/KOMODO_INIT_ADMIN_PASSWORD=${ADMIN_PASSWORD}/" "$COMPOSE_ENV"
|
||||
sed -i "s/^KOMODO_WEBHOOK_SECRET=.*/KOMODO_WEBHOOK_SECRET=${WEBHOOK_SECRET}/" "$COMPOSE_ENV"
|
||||
sed -i "s/^KOMODO_JWT_SECRET=.*/KOMODO_JWT_SECRET=${JWT_SECRET}/" "$COMPOSE_ENV"
|
||||
sed -i "s|^KOMODO_HOST=.*|KOMODO_HOST=http://${LOCAL_IP}:${DEFAULT_PORT}|" "$COMPOSE_ENV"
|
||||
msg_ok "Configured environment"
|
||||
|
||||
msg_info "Starting ${APP}"
|
||||
@@ -214,14 +215,23 @@ function install() {
|
||||
} >>~/komodo.creds
|
||||
|
||||
echo ""
|
||||
msg_ok "${APP} is reachable at: ${BL}http://${LOCAL_IP}:${DEFAULT_PORT}${CL}"
|
||||
msg_info "Waiting for ${APP} to answer"
|
||||
for _ in $(seq 1 60); do
|
||||
curl -fsS --max-time 3 "http://127.0.0.1:${DEFAULT_PORT}" >/dev/null 2>&1 && break
|
||||
sleep 3
|
||||
done
|
||||
if curl -fsS --max-time 3 "http://127.0.0.1:${DEFAULT_PORT}" >/dev/null 2>&1; then
|
||||
msg_ok "${APP} is reachable at: ${BL}http://${LOCAL_IP}:${DEFAULT_PORT}${CL}"
|
||||
else
|
||||
msg_error "${APP} did not come up - check: docker logs komodo-core-1"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " Komodo Credentials"
|
||||
echo -e " =================="
|
||||
echo -e " User : admin"
|
||||
echo -e " Password: ${ADMIN_PASSWORD}"
|
||||
echo ""
|
||||
msg_info "Credentials saved to ~/komodo.creds"
|
||||
msg_ok "Credentials saved to ~/komodo.creds"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
__ __ __ __ ____ _____
|
||||
/ / / /___ ___ / /_ ________ / / / __ \/ ___/
|
||||
/ / / / __ `__ \/ __ \/ ___/ _ \/ / / / / /\__ \
|
||||
/ /_/ / / / / / / /_/ / / / __/ / / /_/ /___/ /
|
||||
\____/_/ /_/ /_/_.___/_/ \___/_/ \____//____/
|
||||
|
||||
+86
-568
@@ -1,280 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
|
||||
source /dev/stdin <<<$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/pve/vm-core.func")
|
||||
load_functions
|
||||
|
||||
function header_info {
|
||||
clear
|
||||
cat <<"EOF"
|
||||
__ __ __ __ ____ _____ _ ____ ___
|
||||
/ / / /___ ___ / /_ ________ / / / __ \/ ___/ | | / / |/ /
|
||||
/ / / / __ `__ \/ __ \/ ___/ _ \/ / / / / /\__ \ | | / / /|_/ /
|
||||
/ /_/ / / / / / / /_/ / / / __/ / / /_/ /___/ / | |/ / / / /
|
||||
\____/_/ /_/ /_/_.___/_/ \___/_/ \____//____/ |___/_/ /_/
|
||||
|
||||
EOF
|
||||
}
|
||||
header_info
|
||||
echo -e "\n Loading..."
|
||||
GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//')
|
||||
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
|
||||
METHOD=""
|
||||
APP="Umbrel OS"
|
||||
APP_TYPE="vm"
|
||||
NSAPP="umbrel-os-vm"
|
||||
var_os="umbrel-os"
|
||||
var_version="n.d."
|
||||
|
||||
YW=$(echo "\033[33m")
|
||||
BL=$(echo "\033[36m")
|
||||
HA=$(echo "\033[1;34m")
|
||||
RD=$(echo "\033[01;31m")
|
||||
BGN=$(echo "\033[4;92m")
|
||||
GN=$(echo "\033[1;92m")
|
||||
DGN=$(echo "\033[32m")
|
||||
CL=$(echo "\033[m")
|
||||
|
||||
BOLD=$(echo "\033[1m")
|
||||
BFR="\\r\\033[K"
|
||||
HOLD=" "
|
||||
TAB=" "
|
||||
|
||||
CM="${TAB}✔️${TAB}${CL}"
|
||||
CROSS="${TAB}✖️${TAB}${CL}"
|
||||
INFO="${TAB}💡${TAB}${CL}"
|
||||
OS="${TAB}🖥️${TAB}${CL}"
|
||||
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
|
||||
DISKSIZE="${TAB}💾${TAB}${CL}"
|
||||
CPUCORE="${TAB}🧠${TAB}${CL}"
|
||||
RAMSIZE="${TAB}🛠️${TAB}${CL}"
|
||||
CONTAINERID="${TAB}🆔${TAB}${CL}"
|
||||
HOSTNAME="${TAB}🏠${TAB}${CL}"
|
||||
BRIDGE="${TAB}🌉${TAB}${CL}"
|
||||
GATEWAY="${TAB}🌐${TAB}${CL}"
|
||||
DEFAULT="${TAB}⚙️${TAB}${CL}"
|
||||
MACADDRESS="${TAB}🔗${TAB}${CL}"
|
||||
VLANTAG="${TAB}🏷️${TAB}${CL}"
|
||||
CREATING="${TAB}🚀${TAB}${CL}"
|
||||
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||||
|
||||
THIN="discard=on,ssd=1,"
|
||||
|
||||
header_info
|
||||
echo -e "\n Loading..."
|
||||
set -e
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
trap cleanup EXIT
|
||||
trap 'post_update_to_api "failed" "130"' SIGINT
|
||||
trap 'post_update_to_api "failed" "143"' SIGTERM
|
||||
trap 'post_update_to_api "failed" "129"; exit 129' SIGHUP
|
||||
function error_handler() {
|
||||
local exit_code="$?"
|
||||
local line_number="$1"
|
||||
local command="$2"
|
||||
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||||
post_update_to_api "failed" "${exit_code}"
|
||||
echo -e "\n$error_message\n"
|
||||
cleanup_vmid
|
||||
}
|
||||
|
||||
function get_valid_nextid() {
|
||||
local try_id
|
||||
try_id=$(pvesh get /cluster/nextid)
|
||||
while true; do
|
||||
if [ -f "/etc/pve/qemu-server/${try_id}.conf" ] || [ -f "/etc/pve/lxc/${try_id}.conf" ]; then
|
||||
try_id=$((try_id + 1))
|
||||
continue
|
||||
fi
|
||||
if lvs --noheadings -o lv_name | grep -qE "(^|[-_])${try_id}($|[-_])"; then
|
||||
try_id=$((try_id + 1))
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
echo "$try_id"
|
||||
}
|
||||
|
||||
function cleanup_vmid() {
|
||||
if qm status $VMID &>/dev/null; then
|
||||
qm stop $VMID &>/dev/null
|
||||
qm destroy $VMID &>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
local exit_code=$?
|
||||
popd >/dev/null
|
||||
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
post_update_to_api "done" "none"
|
||||
else
|
||||
post_update_to_api "failed" "$exit_code"
|
||||
fi
|
||||
fi
|
||||
rm -rf $TEMP_DIR
|
||||
}
|
||||
vm_require_arch amd64
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
pushd $TEMP_DIR >/dev/null
|
||||
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Umbrel OS VM" --yesno "This will create a New Umbrel OS VM. Proceed?" 10 58; then
|
||||
:
|
||||
else
|
||||
header_info && echo -e "${CROSS}${RD}User exited script${CL}\n" && exit
|
||||
fi
|
||||
|
||||
function msg_info() {
|
||||
local msg="$1"
|
||||
echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}"
|
||||
}
|
||||
|
||||
function msg_ok() {
|
||||
local msg="$1"
|
||||
echo -e "${BFR}${CM}${GN}${msg}${CL}"
|
||||
}
|
||||
|
||||
function msg_error() {
|
||||
local msg="$1"
|
||||
echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
|
||||
}
|
||||
|
||||
function check_root() {
|
||||
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
|
||||
clear
|
||||
msg_error "Please run this script as root."
|
||||
echo -e "\nExiting..."
|
||||
sleep 2
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
|
||||
# Supported: Proxmox VE 8.0.x – 8.9.x, 9.0 and 9.2
|
||||
pve_check() {
|
||||
local PVE_VER
|
||||
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||||
|
||||
# Check for Proxmox VE 8.x: allow 8.0–8.9
|
||||
if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
|
||||
local MINOR="${BASH_REMATCH[1]}"
|
||||
if ((MINOR < 0 || MINOR > 9)); then
|
||||
msg_error "This version of Proxmox VE is not supported."
|
||||
msg_error "Supported: Proxmox VE version 8.0 – 8.9"
|
||||
exit 105
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for Proxmox VE 9.x: allow 9.0 and 9.2
|
||||
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
|
||||
local MINOR="${BASH_REMATCH[1]}"
|
||||
if ((MINOR < 0 || MINOR > 2)); then
|
||||
msg_error "This version of Proxmox VE is not supported."
|
||||
msg_error "Supported: Proxmox VE version 9.0 – 9.2"
|
||||
exit 105
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# All other unsupported versions
|
||||
msg_error "This version of Proxmox VE is not supported."
|
||||
msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0 – 9.2"
|
||||
exit 105
|
||||
}
|
||||
|
||||
function arch_check() {
|
||||
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
|
||||
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
|
||||
echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
|
||||
echo -e "Exiting..."
|
||||
sleep 2
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
function ssh_check() {
|
||||
if command -v pveversion >/dev/null 2>&1; then
|
||||
if [ -n "${SSH_CLIENT:+x}" ]; then
|
||||
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's suggested to use the Proxmox shell instead of SSH, since SSH can create issues while gathering variables. Would you like to proceed with using SSH?" 10 62; then
|
||||
echo "you've been warned"
|
||||
else
|
||||
clear
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function exit-script() {
|
||||
clear
|
||||
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||||
exit
|
||||
}
|
||||
|
||||
# Ensure pv is installed or abort with instructions
|
||||
function ensure_pv() {
|
||||
if ! command -v pv &>/dev/null; then
|
||||
msg_info "Installing required package: pv"
|
||||
if ! apt-get update -qq &>/dev/null || ! apt-get install -y pv &>/dev/null; then
|
||||
msg_error "Failed to install pv automatically."
|
||||
echo -e "\nPlease run manually on the Proxmox host:\n apt install pv\n"
|
||||
exit 237
|
||||
fi
|
||||
msg_ok "Installed pv"
|
||||
fi
|
||||
}
|
||||
|
||||
# Download an .xz file and validate it
|
||||
# Args: $1=url $2=cache_file
|
||||
function download_and_validate_xz() {
|
||||
local url="$1"
|
||||
local file="$2"
|
||||
|
||||
# If file exists, check validity
|
||||
if [[ -s "$file" ]]; then
|
||||
if xz -t "$file" &>/dev/null; then
|
||||
msg_ok "Using cached image $(basename "$file")"
|
||||
return 0
|
||||
else
|
||||
msg_error "Cached file $(basename "$file") is corrupted. Deleting and retrying download..."
|
||||
rm -f "$file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Download fresh file
|
||||
msg_info "Downloading image: $(basename "$file")"
|
||||
if ! curl -fSL -o "$file" "$url"; then
|
||||
msg_error "Download failed: $url"
|
||||
rm -f "$file"
|
||||
exit 115
|
||||
fi
|
||||
|
||||
# Validate again
|
||||
if ! xz -t "$file" &>/dev/null; then
|
||||
msg_error "Downloaded file $(basename "$file") is corrupted. Please try again later."
|
||||
rm -f "$file"
|
||||
exit 115
|
||||
fi
|
||||
msg_ok "Downloaded and validated $(basename "$file")"
|
||||
}
|
||||
|
||||
# Extract .xz with pv
|
||||
# Args: $1=cache_file $2=target_img
|
||||
function extract_xz_with_pv() {
|
||||
set -o pipefail
|
||||
local file="$1"
|
||||
local target="$2"
|
||||
|
||||
msg_info "Decompressing $(basename "$file") to $target"
|
||||
if ! xz -dc "$file" | pv -N "Extracting" >"$target"; then
|
||||
msg_error "Failed to extract $file"
|
||||
rm -f "$target"
|
||||
exit 115
|
||||
fi
|
||||
msg_ok "Decompressed to $target"
|
||||
}
|
||||
|
||||
function default_settings() {
|
||||
vm_apply_machine_type "q35"
|
||||
VMID=$(get_valid_nextid)
|
||||
MACHINE=" -machine q35"
|
||||
FORMAT=""
|
||||
DISK_SIZE="32G"
|
||||
HN="umbrelos"
|
||||
CPU_TYPE=""
|
||||
@@ -286,223 +49,27 @@ function default_settings() {
|
||||
MTU=""
|
||||
START_VM="yes"
|
||||
METHOD="default"
|
||||
echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}${VMID}${CL}"
|
||||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
|
||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE}${CL}"
|
||||
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}"
|
||||
echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
|
||||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
||||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE}${CL}"
|
||||
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}"
|
||||
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}${MAC}${CL}"
|
||||
echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}Default${CL}"
|
||||
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}"
|
||||
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
|
||||
echo -e "${CREATING}${BOLD}${DGN}Creating a Umbrel OS VM using the above default settings${CL}"
|
||||
vm_echo_default_settings
|
||||
}
|
||||
|
||||
function advanced_settings() {
|
||||
METHOD="advanced"
|
||||
[ -z "${VMID:-}" ] && VMID=$(get_valid_nextid)
|
||||
while true; do
|
||||
if VMID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Virtual Machine ID" 8 58 $VMID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z "$VMID" ]; then
|
||||
VMID=$(get_valid_nextid)
|
||||
fi
|
||||
if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
|
||||
echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
echo -e "${CONTAINERID}${BOLD}${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
|
||||
break
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
done
|
||||
vm_prompt_vmid "${VMID:-$(get_valid_nextid)}"
|
||||
vm_prompt_machine_type "q35"
|
||||
vm_prompt_disk_size "32G"
|
||||
vm_prompt_disk_cache "none"
|
||||
vm_prompt_hostname "umbrelos"
|
||||
vm_prompt_cpu_model "kvm64"
|
||||
vm_prompt_cpu_cores "2"
|
||||
vm_prompt_ram "4096"
|
||||
vm_prompt_bridge "vmbr0"
|
||||
vm_prompt_mac "$GEN_MAC"
|
||||
vm_prompt_vlan
|
||||
vm_prompt_mtu
|
||||
vm_prompt_verbose "no"
|
||||
vm_prompt_start_vm "yes"
|
||||
|
||||
if MACH=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "MACHINE TYPE" --radiolist --cancel-button Exit-Script "Choose Machine Type" 10 58 2 \
|
||||
"q35" "Modern (PCIe, UEFI, default)" ON \
|
||||
"i440fx" "Legacy (older compatibility)" OFF \
|
||||
3>&1 1>&2 2>&3); then
|
||||
if [ "$MACH" = "q35" ]; then
|
||||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}q35${CL}"
|
||||
FORMAT=""
|
||||
MACHINE=" -machine q35"
|
||||
else
|
||||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Machine Type: ${BGN}i440fx${CL}"
|
||||
FORMAT=",efitype=4m"
|
||||
MACHINE=""
|
||||
fi
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
|
||||
if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GiB (e.g., 10, 20)" 8 58 "$DISK_SIZE" --title "DISK SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
DISK_SIZE=$(echo "$DISK_SIZE" | tr -d ' ')
|
||||
if [[ "$DISK_SIZE" =~ ^[0-9]+$ ]]; then
|
||||
DISK_SIZE="${DISK_SIZE}G"
|
||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
|
||||
elif [[ "$DISK_SIZE" =~ ^[0-9]+G$ ]]; then
|
||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}$DISK_SIZE${CL}"
|
||||
else
|
||||
echo -e "${DISKSIZE}${BOLD}${RD}Invalid Disk Size. Please use a number (e.g., 10 or 10G).${CL}"
|
||||
exit-script
|
||||
fi
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
|
||||
if DISK_CACHE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISK CACHE" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
|
||||
"0" "None (Default)" ON \
|
||||
"1" "Write Through" OFF \
|
||||
3>&1 1>&2 2>&3); then
|
||||
if [ $DISK_CACHE = "1" ]; then
|
||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}Write Through${CL}"
|
||||
DISK_CACHE="cache=writethrough,"
|
||||
else
|
||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Cache: ${BGN}None${CL}"
|
||||
DISK_CACHE=""
|
||||
fi
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
|
||||
if VM_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 umbrelos --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z $VM_NAME ]; then
|
||||
HN="umbrelos"
|
||||
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
|
||||
else
|
||||
HN=$(echo "${VM_NAME,,}" | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//')
|
||||
if [ "$HN" != "${VM_NAME,,}" ]; then
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "HOSTNAME ADJUSTED" --msgbox "Invalid characters detected. Hostname has been adjusted to:\n\n $HN" 10 58
|
||||
fi
|
||||
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
|
||||
fi
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
|
||||
if CPU_TYPE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CPU MODEL" --radiolist "Choose CPU Model" --cancel-button Exit-Script 10 58 2 \
|
||||
"KVM64" "Default – safe for migration/compatibility" ON \
|
||||
"Host" "Use host CPU features (faster, no migration)" OFF \
|
||||
3>&1 1>&2 2>&3); then
|
||||
case "$CPU_TYPE1" in
|
||||
"Host")
|
||||
echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}Host${CL}"
|
||||
CPU_TYPE=" -cpu host"
|
||||
;;
|
||||
*)
|
||||
echo -e "${OS}${BOLD}${DGN}CPU Model: ${BGN}KVM64${CL}"
|
||||
CPU_TYPE=""
|
||||
;;
|
||||
esac
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z "$CORE_COUNT" ]; then CORE_COUNT="2"; fi
|
||||
if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
|
||||
break
|
||||
fi
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "CPU Cores must be a positive integer (e.g., 2)." 8 58
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
done
|
||||
|
||||
while true; do
|
||||
if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 2048 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z "$RAM_SIZE" ]; then RAM_SIZE="2048"; fi
|
||||
if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}$RAM_SIZE${CL}"
|
||||
break
|
||||
fi
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "RAM Size must be a positive integer in MiB (e.g., 2048)." 8 58
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
done
|
||||
|
||||
if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z $BRG ]; then
|
||||
BRG="vmbr0"
|
||||
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
||||
else
|
||||
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
||||
fi
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z "$MAC1" ]; then
|
||||
MAC="$GEN_MAC"
|
||||
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}"
|
||||
break
|
||||
fi
|
||||
if [[ "$MAC1" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then
|
||||
MAC="$MAC1"
|
||||
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
|
||||
break
|
||||
fi
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "Invalid MAC address format. Use XX:XX:XX:XX:XX:XX (e.g., AA:BB:CC:DD:EE:FF)." 8 58
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
done
|
||||
|
||||
while true; do
|
||||
if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z "$VLAN1" ]; then
|
||||
VLAN1="Default"
|
||||
VLAN=""
|
||||
echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
|
||||
break
|
||||
fi
|
||||
if [[ "$VLAN1" =~ ^[0-9]+$ ]] && [ "$VLAN1" -ge 1 ] && [ "$VLAN1" -le 4094 ]; then
|
||||
VLAN=",tag=$VLAN1"
|
||||
echo -e "${VLANTAG}${BOLD}${DGN}VLAN: ${BGN}$VLAN1${CL}"
|
||||
break
|
||||
fi
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "VLAN must be a number between 1 and 4094, or leave blank for default." 8 58
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
done
|
||||
|
||||
while true; do
|
||||
if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
||||
if [ -z "$MTU1" ]; then
|
||||
MTU1="Default"
|
||||
MTU=""
|
||||
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
|
||||
break
|
||||
fi
|
||||
if [[ "$MTU1" =~ ^[0-9]+$ ]] && [ "$MTU1" -ge 576 ] && [ "$MTU1" -le 65520 ]; then
|
||||
MTU=",mtu=$MTU1"
|
||||
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
|
||||
break
|
||||
fi
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --title "INVALID INPUT" --msgbox "MTU Size must be a number between 576 and 65520, or leave blank for default." 8 58
|
||||
else
|
||||
exit-script
|
||||
fi
|
||||
done
|
||||
|
||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "START VIRTUAL MACHINE" --yesno "Start VM when completed?" 10 58); then
|
||||
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}yes${CL}"
|
||||
START_VM="yes"
|
||||
else
|
||||
echo -e "${GATEWAY}${BOLD}${DGN}Start VM when completed: ${BGN}no${CL}"
|
||||
START_VM="no"
|
||||
fi
|
||||
|
||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create a Umbrel OS VM?" --no-button Do-Over 10 58); then
|
||||
if vm_confirm_advanced_settings "Ready to create a Umbrel OS VM?"; then
|
||||
echo -e "${CREATING}${BOLD}${DGN}Creating a Umbrel OS VM using the above advanced settings${CL}"
|
||||
else
|
||||
header_info
|
||||
@@ -511,128 +78,79 @@ function advanced_settings() {
|
||||
fi
|
||||
}
|
||||
|
||||
function start_script() {
|
||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58); then
|
||||
header_info
|
||||
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings${CL}"
|
||||
default_settings
|
||||
else
|
||||
header_info
|
||||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
|
||||
advanced_settings
|
||||
fi
|
||||
}
|
||||
|
||||
check_root
|
||||
arch_check
|
||||
pve_check
|
||||
ssh_check
|
||||
ensure_pv
|
||||
start_script
|
||||
vm_preflight
|
||||
vm_start_script "Use Default Settings?\n\nDefaults:\n• 2 CPU Cores\n• 4 GB RAM\n• 32 GB Disk" 13 58
|
||||
post_to_api_vm
|
||||
|
||||
msg_info "Validating Storage"
|
||||
STORAGE_MENU=()
|
||||
while read -r tag type free; do
|
||||
ITEM="Type: $type Free: $free"
|
||||
STORAGE_MENU+=("$tag" "$ITEM" "OFF")
|
||||
done < <(pvesm status -content images | awk 'NR>1 {printf "%s %s %s\n", $1, $2, $6}')
|
||||
vm_select_storage "$HN"
|
||||
|
||||
if [ ${#STORAGE_MENU[@]} -eq 0 ]; then
|
||||
msg_error "Unable to detect a valid storage location."
|
||||
exit 119
|
||||
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
|
||||
STORAGE=${STORAGE_MENU[0]}
|
||||
else
|
||||
while [ -z "${STORAGE:+x}" ]; do
|
||||
STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
|
||||
"Which storage pool would you like to use for ${HN}?\nTo make a selection, use the Spacebar.\n" \
|
||||
16 70 6 \
|
||||
"${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3)
|
||||
done
|
||||
fi
|
||||
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
|
||||
|
||||
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."
|
||||
msg_info "Retrieving the URL for the Umbrel OS installer ISO"
|
||||
UMBREL_RELEASE="$(curl -fsSL --max-time 20 https://api.umbrel.com/latest-release 2>/dev/null |
|
||||
sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
[[ -z "$UMBREL_RELEASE" ]] && UMBREL_RELEASE="latest"
|
||||
var_version="$UMBREL_RELEASE"
|
||||
|
||||
URL="https://download.umbrel.com/release/latest/umbrelos-amd64.img.xz"
|
||||
CACHE_DIR="/var/lib/vz/template/cache"
|
||||
CACHE_FILE="$CACHE_DIR/$(basename "$URL")"
|
||||
FILE_IMG="/var/lib/vz/template/tmp/${CACHE_FILE##*/%.xz}"
|
||||
URL="https://download.umbrel.com/release/${UMBREL_RELEASE}/umbrelos-amd64-usb-installer.iso"
|
||||
# The upstream file name is the same for every release, so the version goes into
|
||||
# the cached name -- otherwise the cache serves 1.7.4 to someone asking for 2.0.
|
||||
ISO_NAME="umbrelos-${UMBREL_RELEASE}-amd64-usb-installer.iso"
|
||||
CACHE_DIR="/var/lib/vz/template/iso"
|
||||
CACHE_FILE="${CACHE_DIR}/${ISO_NAME}"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
msg_ok "${CL}${BL}${URL}${CL}"
|
||||
|
||||
mkdir -p "$CACHE_DIR" "$(dirname "$FILE_IMG")"
|
||||
# download.umbrel.com answers 307 for any name at all, so a redirect proves
|
||||
# nothing about the file existing. Size is what separates an ISO from a 404 page.
|
||||
msg_info "Downloading the Umbrel OS installer ISO (approximately 1.8 GB)"
|
||||
vm_fetch_image "$URL" "$CACHE_FILE" --cache --min-bytes $((1024 * 1024 * 1024)) || exit 115
|
||||
|
||||
download_and_validate_xz "$URL" "$CACHE_FILE"
|
||||
|
||||
qm create $VMID${MACHINE} -bios ovmf -agent 1 -tablet 0 -localtime 1 ${CPU_TYPE} \
|
||||
msg_info "Creating a Umbrel OS VM"
|
||||
# Umbrel requires EFI: the installer ISO has no legacy boot path. Its own
|
||||
# console runs on tty1, so this VM is driven through noVNC, not the serial line.
|
||||
qm create "$VMID"${MACHINE} -bios ovmf -agent enabled=1 -tablet 0 -localtime 1 ${CPU_TYPE} \
|
||||
-cores "$CORE_COUNT" -memory "$RAM_SIZE" -name "$HN" -tags community-script \
|
||||
-net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci >/dev/null
|
||||
|
||||
extract_xz_with_pv "$CACHE_FILE" "$FILE_IMG"
|
||||
|
||||
if qm disk import --help >/dev/null 2>&1; then
|
||||
IMPORT_CMD=(qm disk import)
|
||||
else
|
||||
IMPORT_CMD=(qm importdisk)
|
||||
fi
|
||||
IMPORT_OUT="$("${IMPORT_CMD[@]}" "$VMID" "$FILE_IMG" "$STORAGE" --format raw 2>&1 || true)"
|
||||
DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*imported disk '\([^']\+\)'.*/\1/p" | tr -d "\r\"'")"
|
||||
[[ -z "$DISK_REF" ]] && DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$5 ~ ("vm-"id"-disk-") {print $1":"$5}' | sort | tail -n1)"
|
||||
|
||||
qm set $VMID \
|
||||
--efidisk0 ${STORAGE}:0,efitype=4m \
|
||||
--scsi0 ${DISK_REF},ssd=1,discard=on \
|
||||
--boot order=scsi0 \
|
||||
--serial0 socket >/dev/null
|
||||
qm set $VMID --agent enabled=1 >/dev/null
|
||||
qm resize $VMID scsi0 ${DISK_SIZE} >/dev/null
|
||||
|
||||
DESCRIPTION=$(
|
||||
cat <<EOF
|
||||
<div align='center'>
|
||||
<a href='https://community-scripts.org' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||||
</a>
|
||||
|
||||
<h2 style='font-size: 24px; margin: 20px 0;'>Umbrel OS VM</h2>
|
||||
|
||||
<p style='margin: 16px 0;'>
|
||||
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||||
</span>
|
||||
</div>
|
||||
EOF
|
||||
)
|
||||
qm set $VMID -description "$DESCRIPTION" >/dev/null
|
||||
|
||||
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Image Cache" \
|
||||
--yesno "Keep downloaded Umbrel OS image for future VMs?\n\nFile: $CACHE_FILE" 10 70; then
|
||||
msg_ok "Keeping cached image"
|
||||
else
|
||||
rm -f "$CACHE_FILE"
|
||||
msg_ok "Deleted cached image"
|
||||
fi
|
||||
rm -f "$FILE_IMG"
|
||||
-net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \
|
||||
-efidisk0 "${STORAGE}:1,efitype=4m,pre-enrolled-keys=0" \
|
||||
-scsi0 "${STORAGE}:${DISK_SIZE%G},${DISK_CACHE:-}${THIN%,}" \
|
||||
-cdrom "local:iso/${ISO_NAME}" -boot order='scsi0;ide2' >/dev/null
|
||||
|
||||
set_description
|
||||
msg_ok "Created a Umbrel OS VM ${CL}${BL}(${HN})"
|
||||
|
||||
if [[ "${VM_UNATTENDED:-0}" == "1" ]]; then
|
||||
KEEP_IMAGE="${VM_KEEP_IMAGE:-yes}"
|
||||
elif vm_dialog yesno "Image Cache" \
|
||||
"Keep downloaded Umbrel OS installer ISO for future VMs?\n\nFile: $CACHE_FILE" 10 70; then
|
||||
KEEP_IMAGE="yes"
|
||||
else
|
||||
KEEP_IMAGE="no"
|
||||
fi
|
||||
|
||||
if [[ "$KEEP_IMAGE" == "yes" ]]; then
|
||||
msg_ok "Keeping cached ISO"
|
||||
else
|
||||
msg_warn "The ISO is still attached to the VM, so it is removed after the install"
|
||||
KEEP_IMAGE="no"
|
||||
fi
|
||||
|
||||
if [ "$START_VM" == "yes" ]; then
|
||||
msg_info "Starting Umbrel OS VM"
|
||||
qm start $VMID
|
||||
$STD qm start $VMID
|
||||
msg_ok "Started Umbrel OS VM"
|
||||
fi
|
||||
post_update_to_api "done" "none"
|
||||
|
||||
echo -e "\n${INFO}${BOLD}${YW}Next Steps:${CL}"
|
||||
echo -e "${TAB}1. Open the VM console in Proxmox (noVNC)"
|
||||
echo -e "${TAB}2. The installer asks which storage device to install umbrelOS on."
|
||||
echo -e "${TAB} Pick the ${BL}sda${CL} entry -- ${BL}sr0${CL} is the installer ISO itself"
|
||||
echo -e "${TAB}3. Confirm, wait for it to finish, then press a key to power off"
|
||||
echo -e "${TAB}4. Detach the ISO (${BL}qm set ${VMID} --ide2 none${CL}) and start the VM"
|
||||
echo -e "${TAB}5. umbrelOS is then reachable at ${BL}http://umbrel.local${CL}"
|
||||
if [[ "$KEEP_IMAGE" == "no" ]]; then
|
||||
echo -e "${TAB} Delete ${BL}${CACHE_FILE}${CL} once the ISO is detached"
|
||||
fi
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
|
||||
Reference in New Issue
Block a user