Upload via GUI (19 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 07:03:08 +02:00
parent fb6da6562e
commit ab502f44ef
19 changed files with 10983 additions and 21 deletions
+263
View File
@@ -0,0 +1,263 @@
'use strict';
/*
* services.js externe Dienste & Systemabfragen für AeroMC
* ----------------------------------------------------------------
* Bewusst frei von Electron-Abhängigkeiten, damit headless (mit `node`)
* testbar. Nutzt globales fetch (Node 18+ / Electron 28).
*
* - scanJava() : findet installierte Java-Runtimes
* - getMcVersions() : Mojang-Versionsliste (mit Datei-Cache)
* - modrinthSearch() : Mod-Suche
* - modrinthInstall() : passende Mod-Version in einen Ordner laden
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync, spawnSync } = require('child_process');
const UA = 'AeroMC-Launcher/0.2 (Minecraft-Launcher; contact: viper)';
// ---------------------------------------------------------------------------
// Java-Erkennung
// ---------------------------------------------------------------------------
function javaCandidateDirs() {
const pf = process.env['ProgramFiles'] || 'C:\\Program Files';
const pfx = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
const appdata = process.env['APPDATA'] || '';
const localappdata = process.env['LOCALAPPDATA'] || '';
const vendors = ['Java', 'Eclipse Adoptium', 'Eclipse Foundation', 'Zulu', 'Amazon Corretto',
'BellSoft', 'Microsoft', 'AdoptOpenJDK', 'Semeru', 'JetBrains'];
const roots = [];
for (const base of [pf, pfx]) {
for (const v of vendors) roots.push(path.join(base, v));
}
// Runtimes der offiziellen Minecraft-Launcher
if (appdata) roots.push(path.join(appdata, '.minecraft', 'runtime'));
if (localappdata) roots.push(path.join(localappdata, 'Packages'));
return roots;
}
// findet rekursiv (begrenzte Tiefe) alle java.exe unter einem Startordner
function findJavaExes(root, depth, acc) {
if (depth < 0 || !fs.existsSync(root)) return;
let entries;
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
const full = path.join(root, e.name);
if (e.isFile() && /^javaw?\.exe$/i.test(e.name)) {
// bevorzugt java.exe (Konsole) statt javaw.exe
if (/^java\.exe$/i.test(e.name)) acc.add(full);
} else if (e.isDirectory()) {
// Tiefe begrenzen; typische Struktur .../<jdk>/bin/java.exe
findJavaExes(full, depth - 1, acc);
}
}
}
function probeJava(javaPath) {
// `java -version` schreibt nach stderr; spawnSync liefert stdout UND stderr.
try {
const r = spawnSync(javaPath, ['-version'], { encoding: 'utf8', timeout: 5000 });
const out = (r.stderr || '') + (r.stdout || '');
if (!out.trim()) return null;
return parseJavaVersion(javaPath, out);
} catch {
return null;
}
}
function parseJavaVersion(javaPath, out) {
const m = out.match(/version "([^"]+)"/i);
const raw = m ? m[1] : '';
let major = 0;
if (raw) {
const parts = raw.split('.');
major = raw.startsWith('1.') ? parseInt(parts[1], 10) : parseInt(parts[0], 10);
}
const arch = /64-bit/i.test(out) ? 'x64' : (/32-bit|Client VM/i.test(out) ? 'x86' : '?');
const vendorLine = out.split('\n').map((l) => l.trim()).find((l) => /runtime|openjdk|hotspot|jre|jdk/i.test(l)) || '';
return { path: javaPath, version: raw || 'unbekannt', major: major || 0, arch, info: vendorLine };
}
function scanJava() {
const found = new Set();
// 1) JAVA_HOME
if (process.env.JAVA_HOME) {
const p = path.join(process.env.JAVA_HOME, 'bin', 'java.exe');
if (fs.existsSync(p)) found.add(p);
}
// 2) java aus PATH
try {
const where = execFileSync('where', ['java'], { encoding: 'utf8', timeout: 4000 });
where.split(/\r?\n/).forEach((l) => { l = l.trim(); if (l && /java\.exe$/i.test(l) && fs.existsSync(l)) found.add(l); });
} catch { /* nix */ }
// 3) bekannte Vendor-Ordner
for (const root of javaCandidateDirs()) findJavaExes(root, 4, found);
// prüfen & dedupen nach Version+Pfad
const results = [];
for (const p of found) {
const info = probeJava(p);
if (info) results.push(info);
}
// sortieren: 64-bit vor 32-bit, höhere Major-Version zuerst
results.sort((a, b) => {
if ((b.arch === 'x64') - (a.arch === 'x64') !== 0) return (b.arch === 'x64') - (a.arch === 'x64');
return b.major - a.major;
});
return results;
}
// ---------------------------------------------------------------------------
// Mojang-Versionsliste (mit Cache)
// ---------------------------------------------------------------------------
const MC_MANIFEST_URL = 'https://piston-meta.mojang.com/mc/game/version_manifest_v2.json';
async function getMcVersions(cacheDir, maxAgeMs = 6 * 3600 * 1000) {
const cacheFile = path.join(cacheDir, 'mc-versions.json');
// Cache frisch genug?
try {
const st = fs.statSync(cacheFile);
if (Date.now() - st.mtimeMs < maxAgeMs) {
return Object.assign({ cached: true }, JSON.parse(fs.readFileSync(cacheFile, 'utf8')));
}
} catch { /* kein Cache */ }
try {
const res = await fetch(MC_MANIFEST_URL, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
const slim = {
latest: data.latest,
versions: data.versions.map((v) => ({ id: v.id, type: v.type, releaseTime: v.releaseTime, url: v.url, sha1: v.sha1 })),
fetchedAt: new Date().toISOString(),
};
try { fs.mkdirSync(cacheDir, { recursive: true }); fs.writeFileSync(cacheFile, JSON.stringify(slim)); } catch { /* egal */ }
return Object.assign({ cached: false }, slim);
} catch (err) {
// offline: alten Cache nehmen, falls vorhanden
try {
const old = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
return Object.assign({ cached: true, offline: true }, old);
} catch {
return { offline: true, error: err.message, latest: {}, versions: [] };
}
}
}
// ---------------------------------------------------------------------------
// Modrinth (Mod-Suche & Installation)
// ---------------------------------------------------------------------------
const MODRINTH = 'https://api.modrinth.com/v2';
const MODRINTH_SORTS = ['relevance', 'downloads', 'follows', 'newest', 'updated'];
async function modrinthSearch({ query = '', gameVersion = '', loader = '', category = '', sort = 'relevance', limit = 30, offset = 0 } = {}) {
const facets = [['project_type:mod']];
if (gameVersion) facets.push(['versions:' + gameVersion]);
if (loader && loader !== 'vanilla') facets.push(['categories:' + loader]);
if (category) facets.push(['categories:' + category]);
const url = new URL(MODRINTH + '/search');
url.searchParams.set('query', query);
url.searchParams.set('limit', String(limit));
url.searchParams.set('offset', String(offset));
url.searchParams.set('index', MODRINTH_SORTS.includes(sort) ? sort : 'relevance');
url.searchParams.set('facets', JSON.stringify(facets));
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Modrinth-Suche fehlgeschlagen: HTTP ' + res.status);
const data = await res.json();
return {
total: data.total_hits,
hits: (data.hits || []).map((h) => ({
projectId: h.project_id,
slug: h.slug,
title: h.title,
description: h.description,
author: h.author,
downloads: h.downloads,
iconUrl: h.icon_url || '',
categories: h.categories || [],
})),
};
}
// findet die passende Datei-Version für Spielversion + Loader
async function modrinthResolveFile({ projectId, gameVersion, loader }) {
const url = new URL(MODRINTH + '/project/' + projectId + '/version');
if (gameVersion) url.searchParams.set('game_versions', JSON.stringify([gameVersion]));
if (loader && loader !== 'vanilla') url.searchParams.set('loaders', JSON.stringify([loader]));
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Versionsabruf fehlgeschlagen: HTTP ' + res.status);
const versions = await res.json();
if (!versions.length) return null;
const v = versions[0]; // neueste passende
const file = v.files.find((f) => f.primary) || v.files[0];
if (!file) return null;
return {
versionId: v.id,
versionNumber: v.version_number,
filename: file.filename,
url: file.url,
sha1: file.hashes && file.hashes.sha1,
size: file.size,
dependencies: v.dependencies || [],
};
}
async function downloadFile(url, destPath) {
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
const buf = Buffer.from(await res.arrayBuffer());
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, buf);
return buf.length;
}
// lädt eine Mod in <modsDir>; gibt Metadaten für die Instanz zurück
async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir }) {
const file = await modrinthResolveFile({ projectId, gameVersion, loader });
if (!file) {
return { ok: false, reason: 'no-match',
message: `Keine passende Version für ${gameVersion} / ${loader} gefunden.` };
}
const dest = path.join(modsDir, file.filename);
const bytes = await downloadFile(file.url, dest);
return {
ok: true,
dependencies: file.dependencies || [],
entry: {
projectId,
title: title || projectId,
filename: file.filename,
versionId: file.versionId,
versionNumber: file.versionNumber,
gameVersion,
loader,
installedAt: new Date().toISOString(),
bytes,
},
};
}
// Projekttitel nachschlagen (für Abhängigkeiten, die nur eine ID liefern)
async function modrinthProjectTitle(projectId) {
try {
const res = await fetch(`${MODRINTH}/project/${projectId}`, { headers: { 'User-Agent': UA } });
if (!res.ok) return projectId;
const data = await res.json();
return data.title || projectId;
} catch { return projectId; }
}
module.exports = {
scanJava, probeJava,
getMcVersions,
modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle, downloadFile,
};