feat(v5): Etappe W-6 — Setup, Selbst-Update, Erst-Einrichtung, Lizenzen
Ampel / ampel (push) Successful in 1m22s

electron-builder → NSIS: EIN RippySetup-<version>.exe, pro Benutzer,
ohne Adminrechte, eigenes Icon (PNG-basiertes ICO selbst gebaut).
extraResources: die MITGELIEFERTEN Werkzeuge (HandBrakeCLI GPL-2,
flac.exe MIT libFLAC.dll — die Ohne-DLL-Falle aus § 9) samt
Lizenztexten und Quellverweisen (bau/lizenzen, Entscheid 9); vendor/
liegt nicht im Git (74 MB), Befuellung steht in BAUEN.md. Der Kern
findet die mitgelieferten Werkzeuge ueber RIPPY_VENDOR (resourcesvendor) VOR allen anderen Suchwegen.

haupt/update.ts (§ 6.8): electron-updater gegen das oeffentliche Gitea
(festes Tag 'aktuell' als Rueckfallebene, Entscheid 6) — NUR https
(http wird ABGELEHNT; Regel-Tests), NIE waehrend eines Rips (der Haupt
kennt die aktiven Vorgaenge), Fehlschlaege still aber ablesbar
(letzte erfolgreiche Pruefung im Status), installiert beim naechsten
Start. haupt/autostart.ts: HKCU-Run ueber app.setLoginItemSettings,
Toggle in den Einstellungen. Erst-Einrichtung (§ 6.9) im Programm:
5 Schritte, NUR ein Fehler blockiert (kein Laufwerk = Warnung,
MakeMKV-Pflicht mit Klartext-Weg).

GEBAUT UND GEMESSEN (30.08.2026):
- dist-setup/RippySetup-5.0.0-w0.exe — 130,8 MB, SHA-256
  07D39D6BAB74A47C21AA53D847FB5A5DB6990AE9FA1CBC7BE814B51050883115,
  plus .blockmap.
- MESSUNG: Die Update-Auskunft heisst bei Vorab-Versionen nach dem
  KANAL (w0.yml) — erst ein Release ohne Suffix erzeugt latest.yml
  (steht jetzt in BAUEN.md).
- Das GEPACKTE Programm (win-unpacked, asar + resources) besteht den
  Smoke: SMOKE OK, und der Screenshot zeigt die Erst-Einrichtung —
  das Bild eines frischen Rechners.

Bewusst offen (SAVEPOINT): die automatische MakeMKV-Beschaffung
(beschaffen.ts — Download+Installer-Start braucht einen Live-Test);
das Setup NENNT den Weg klar. 185 Tests gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 19:03:40 +02:00
co-authored by Claude Fable 5
parent 2d9cef22b3
commit 9fa3df500d
21 changed files with 4319 additions and 8 deletions
+4
View File
@@ -52,4 +52,8 @@ contextBridge.exposeInMainWorld('rippy', {
ordnerWaehlen(titel: string): Promise<string | null> {
return ipcRenderer.invoke('ordner-waehlen', String(titel)) as Promise<string | null>
},
/** Autostart an/aus (§ 8) — liefert den tatsächlichen Zustand zurück. */
autostartSetzen(an: boolean): Promise<boolean> {
return ipcRenderer.invoke('autostart-setzen', an === true) as Promise<boolean>
},
})
+198 -4
View File
@@ -248,7 +248,7 @@ function PresetWahl({
)
}
function Einstellungen({ verbindung }: { verbindung: Verbindung }) {
function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: HauptStatus | null }) {
const werte = verbindung.einstellungen
const presets = verbindung.werkzeuge?.presets ?? []
const setzen = einstellungSetzen
@@ -353,6 +353,31 @@ function Einstellungen({ verbindung }: { verbindung: Verbindung }) {
</div>
</section>
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Programm</h2>
<label className="flex items-center gap-2 text-xs text-slate-300">
<input
type="checkbox"
checked={haupt?.autostart === true}
onChange={(e) => void window.rippy.autostartSetzen(e.target.checked)}
/>
Rippy mit Windows starten (HKCU\\Run kein Dienst, keine Adminrechte)
</label>
<Feld
name="Update-Adresse"
hinweis="leer = Gitea des Commanders; NUR https:// wird angenommen (§ 6.8)"
wert={werte['updateUrl'] ?? ''}
aufAendern={(w) => setzen('updateUrl', w)}
/>
{haupt !== null && (
<p className="font-mono text-xs text-slate-400">
Selbst-Update: {haupt.update.text}
{haupt.update.letzteErfolgreichePruefung.length > 0 &&
` · letzte erfolgreiche Prüfung: ${haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}`}
</p>
)}
</section>
<section className="space-y-3">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Werkzeuge</h2>
<div className="space-y-1 rounded-xl border border-slate-700/60 bg-slate-800/40 p-4 font-mono text-xs">
@@ -418,6 +443,169 @@ function Bibliothek({ verbindung }: { verbindung: Verbindung }) {
)
}
// ── Erst-Einrichtung (§ 6.9): nach der Installation, im Programm ────────
// Zwei Regeln aus dem alten Setup: NUR ein Fehler blockiert (kein
// Laufwerk ist eine WARNUNG — eine reine Komprimier-Maschine ist ein
// vorgesehener Betriebsfall), und Erfolg wird am BESTAND gemessen.
function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
const [schritt, setSchritt] = useState(0)
const werkzeuge = verbindung.werkzeuge
const makemkvFehlt = werkzeuge !== null && werkzeuge.makemkvcon.length === 0
const pruefzeilen: Array<{ ok: boolean | null; pflicht: boolean; text: string }> = [
{
ok: werkzeuge === null ? null : werkzeuge.makemkvcon.length > 0,
pflicht: true,
text:
werkzeuge === null
? 'MakeMKV wird gesucht …'
: werkzeuge.makemkvcon.length > 0
? `MakeMKV gefunden: ${werkzeuge.makemkvcon}`
: 'MakeMKV FEHLT — ohne makemkvcon kann Rippy keine Video-Disc lesen. Von makemkv.com/download installieren, dann findet Rippy es von selbst.',
},
{
ok: werkzeuge === null ? null : werkzeuge.handbrake.length > 0,
pflicht: false,
text:
werkzeuge === null
? 'HandBrakeCLI wird gesucht …'
: werkzeuge.handbrake.length > 0
? `HandBrakeCLI dabei: ${werkzeuge.handbrake}`
: 'HandBrakeCLI fehlt — Rippen geht, Komprimieren nicht.',
},
{
ok: werkzeuge === null ? null : werkzeuge.flac.length > 0,
pflicht: false,
text:
werkzeuge === null
? 'FLAC wird gesucht …'
: werkzeuge.flac.length > 0
? `FLAC dabei: ${werkzeuge.flac}`
: 'FLAC fehlt — nur für Audio-CDs nötig.',
},
{
ok: verbindung.laufwerke.length > 0 ? true : null,
pflicht: false,
text:
verbindung.laufwerke.length > 0
? `${verbindung.laufwerke.length} optisches Laufwerk gefunden (${verbindung.laufwerke.map((l) => l.id + ':').join(', ')})`
: 'Kein optisches Laufwerk — nur eine Warnung: Eine reine Komprimier-Maschine ist ein vorgesehener Betriebsfall.',
},
]
const schritte = ['Willkommen', 'Prüfung', 'Ordner', 'Automatik', 'Fertig']
return (
<div className="mx-auto max-w-xl space-y-6">
<div className="flex gap-2">
{schritte.map((name, i) => (
<span
key={name}
className={`rounded-full px-3 py-1 text-xs ${i === schritt ? 'bg-sky-500/20 text-sky-300' : i < schritt ? 'text-emerald-400' : 'text-slate-500'}`}
>
{i + 1} {name}
</span>
))}
</div>
{schritt === 0 && (
<div className="space-y-3 text-sm text-slate-300">
<h2 className="text-xl font-bold text-slate-100">Willkommen bei Rippy</h2>
<p>Rippy sichert deine DVDs, Blu-rays und Audio-CDs erst verlustfrei, dann komprimiert.</p>
<p>Disc rein genügt: Rippy erkennt, was drauf ist, und sagt Bescheid.</p>
<p>Fertige Filme landen so abgelegt, dass Jellyfin, Emby, Kodi und Plex sie sofort erkennen.</p>
<p>Dieses Setup stellt vier Dinge ein und dauert eine Minute.</p>
</div>
)}
{schritt === 1 && (
<div className="space-y-2">
{pruefzeilen.map((zeile, i) => (
<p
key={i}
className={`font-mono text-xs ${zeile.ok === true ? 'text-emerald-300' : zeile.ok === null ? 'text-slate-400' : zeile.pflicht ? 'text-red-300' : 'text-amber-300'}`}
>
{zeile.ok === true ? '✓' : zeile.ok === null ? '…' : zeile.pflicht ? '✗' : '⚠'} {zeile.text}
</p>
))}
</div>
)}
{schritt === 2 && (
<div className="space-y-3">
<p className="text-sm text-slate-300">Wohin sollen fertige Filme, Musik und Abbilder?</p>
<p className="break-all font-mono text-xs text-slate-400">{verbindung.einstellungen['ablage'] ?? ''}</p>
<button
onClick={() =>
void window.rippy.ordnerWaehlen('Ablage-Ordner wählen').then((ordner) => {
if (ordner !== null) einstellungSetzen('ablage', ordner)
})
}
className="rounded-lg border border-slate-600 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700"
>
Ordner wählen (auch Netzlaufwerke)
</button>
</div>
)}
{schritt === 3 && (
<div className="space-y-3">
<p className="text-sm text-slate-300">Wie viel soll Rippy allein entscheiden?</p>
<label className="flex items-center gap-2 text-xs text-slate-300">
<input
type="checkbox"
checked={verbindung.einstellungen['transcodeEnabled'] !== 'false'}
onChange={(e) => einstellungSetzen('transcodeEnabled', e.target.checked ? 'true' : 'false')}
/>
Nach dem Rip automatisch komprimieren (je Disc-Typ später fein einstellbar)
</label>
<p className="text-xs text-slate-500">
Alles Weitere Presets je Disc-Typ, Sprachen, Medienserver, API-Schlüssel steht unter
Einstellungen".
</p>
</div>
)}
{schritt === 4 && (
<div className="space-y-3 text-sm text-slate-300">
<p>Das war's. Disc einlegen — Rippy meldet sich.</p>
{makemkvFehlt && (
<p className="text-xs text-red-300">
Denk an MakeMKV: Ohne makemkvcon bleibt der Video-Weg zu (Einstellungen → Werkzeuge zeigt
den Stand, sobald es installiert ist).
</p>
)}
</div>
)}
<div className="flex justify-between">
<button
onClick={() => setSchritt((s) => Math.max(0, s - 1))}
disabled={schritt === 0}
className="rounded-lg border border-slate-600 px-4 py-1.5 text-xs text-slate-300 hover:bg-slate-700 disabled:opacity-40"
>
Zurück
</button>
{schritt < schritte.length - 1 ? (
<button
onClick={() => setSchritt((s) => s + 1)}
className="rounded-lg border border-sky-500/60 px-4 py-1.5 text-xs text-sky-300 hover:bg-sky-500/10"
>
Weiter
</button>
) : (
<button
onClick={() => einstellungSetzen('setup.done', 'true')}
className="rounded-lg border border-emerald-500/60 px-4 py-1.5 text-xs text-emerald-300 hover:bg-emerald-500/10"
>
Los geht's
</button>
)}
</div>
</div>
)
}
type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek'
export default function App() {
@@ -447,6 +635,11 @@ export default function App() {
const db = verbindung.kern?.datenbank ?? null
// § 6.9: Die Erst-Einrichtung läuft NACH der Installation, im Programm —
// solange sie nicht abgeschlossen ist, ist sie die ganze Ansicht.
const setupOffen =
Object.keys(verbindung.einstellungen).length > 0 && verbindung.einstellungen['setup.done'] !== 'true'
const knopf = (ziel: Bereich, text: string): React.JSX.Element => (
<button
onClick={() => setBereich(ziel)}
@@ -477,7 +670,8 @@ export default function App() {
</header>
<main className="flex-1 space-y-6 p-6">
{bereich === 'uebersicht' && (
{setupOffen && <ErstEinrichtung verbindung={verbindung} />}
{!setupOffen && bereich === 'uebersicht' && (
<>
<section>
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
@@ -581,8 +775,8 @@ export default function App() {
</>
)}
{bereich === 'einstellungen' && <Einstellungen verbindung={verbindung} />}
{bereich === 'bibliothek' && <Bibliothek verbindung={verbindung} />}
{!setupOffen && bereich === 'einstellungen' && <Einstellungen verbindung={verbindung} haupt={haupt} />}
{!setupOffen && bereich === 'bibliothek' && <Bibliothek verbindung={verbindung} />}
</main>
<footer className="border-t border-slate-700/60 px-6 py-3 text-center text-xs text-slate-500">
+1
View File
@@ -8,6 +8,7 @@ declare global {
aufStatus(horcher: (status: unknown) => void): void
melden(art: string): void
ordnerWaehlen(titel: string): Promise<string | null>
autostartSetzen(an: boolean): Promise<boolean>
}
}
}
@@ -131,6 +131,9 @@ export interface HauptStatus {
/** Prozess-Leine (Job Object, § 3.3): gesetzt oder mit Fehlertext. */
leine: { ok: boolean; fehler?: string }
kern: { laeuft: boolean; pid?: number; neustarts: number }
autostart: boolean
/** Selbst-Update (§ 6.8): letzter Prüf-Erfolg + Klartext-Stand. */
update: { letzteErfolgreichePruefung: string; text: string }
}
function istObjekt(wert: unknown): wert is Record<string, unknown> {
+13
View File
@@ -0,0 +1,13 @@
// Autostart über HKCU\...\Run (KONZEPT § 8) — via Electrons eigener
// API (app.setLoginItemSettings nutzt unter Windows genau diesen
// Registry-Zweig). Kein Dienst: Ein Dienst kostete bei jeder Installation
// eine Administrator-Abfrage und könnte kein Tray zeigen.
import { app } from 'electron'
export function autostartSetzen(an: boolean): void {
app.setLoginItemSettings({ openAtLogin: an })
}
export function autostartAktiv(): boolean {
return app.getLoginItemSettings().openAtLogin
}
+31
View File
@@ -12,10 +12,12 @@ import { app, dialog, ipcMain, Notification, type BrowserWindow } from 'electron
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import type { HauptStatus, KernNachricht } from '../gemeinsam/nachrichten'
import { autostartAktiv, autostartSetzen } from './autostart'
import { fensterErstellen } from './fenster'
import { KernVerwaltung } from './kernstart'
import { leineSetzen, type LeineStatus } from './leine'
import { trayErstellen, trayHinweis } from './tray'
import { updateEinrichten, updateStand } from './update'
const smokeModus = process.argv.includes('--smoke')
@@ -44,6 +46,10 @@ function start(): void {
let fenster: BrowserWindow | null = null
let kernBereit: Extract<KernNachricht, { art: 'kern-bereit' }> | null = null
let beendenGewollt = smokeModus
// § 6.8 Regel 2: Der Haupt weiß, ob irgendwo gerippt wird — Updates
// warten dann.
const aktiveVorgaenge = new Set<string>()
let updateGestartet = false
const smoke = {
fenster: false,
@@ -67,6 +73,17 @@ function start(): void {
smokePruefen()
} else if (nachricht.art === 'rip-status') {
ripStatusVerarbeiten(nachricht.status)
} else if (nachricht.art === 'einstellungen') {
// Einmalig das Selbst-Update einrichten — die Adresse kommt aus
// der EINEN Einstellungs-Quelle im Kern (§ 6.7/§ 6.8).
if (!updateGestartet && !smokeModus) {
updateGestartet = true
updateEinrichten(
nachricht.werte['updateUrl'] ?? '',
() => aktiveVorgaenge.size > 0,
(text) => toast('Rippy-Update', text),
)
}
} else if (nachricht.art === 'disc-info' && nachricht.info.titel.length > 0 && !smokeModus) {
// Windows-Toast beim Erkennen (§ 7) — Klick holt das Fenster hoch.
toast(
@@ -85,6 +102,11 @@ function start(): void {
// § 7: Toasts, Taskleisten-Fortschritt und Tray-Zustand aus dem
// Rip-Status — die Information, die man will, ohne das Fenster zu öffnen.
function ripStatusVerarbeiten(status: Extract<KernNachricht, { art: 'rip-status' }>['status']): void {
if (status.phase === 'rippt' || status.phase === 'komprimiert' || status.phase === 'legt-ab' || status.phase === 'liest-info') {
aktiveVorgaenge.add(status.id)
} else {
aktiveVorgaenge.delete(status.id)
}
if (fenster !== null && !fenster.isDestroyed()) {
if ((status.phase === 'rippt' || status.phase === 'komprimiert') && status.prozent >= 0) {
fenster.setProgressBar(status.prozent / 100)
@@ -126,6 +148,8 @@ function start(): void {
version: app.getVersion(),
leine,
kern: kernVerwaltung.zustand(),
autostart: autostartAktiv(),
update: updateStand(),
}
fenster.webContents.send('haupt-status', status)
}
@@ -173,6 +197,13 @@ function start(): void {
}
})
// Autostart (§ 8): HKCU\...\Run über Electrons eigene API.
ipcMain.handle('autostart-setzen', (_ereignis, an: unknown) => {
autostartSetzen(an === true)
statusSenden()
return autostartAktiv()
})
// Echter Windows-Ordnerdialog (§ 7) — der Nutzer sieht seine
// Netzlaufwerke und Schnellzugriffe statt eines nachgebauten Browsers.
ipcMain.handle('ordner-waehlen', async (_ereignis, titel: unknown) => {
+8 -1
View File
@@ -2,7 +2,7 @@
// utilityProcess statt child_process.fork ist Electrons eigene Empfehlung;
// nur er kann einen MessagePort DIREKT zwischen Kern und Fenster aufspannen
// (Electron-Doku »utilityProcess« / »MessagePorts in Electron«).
import { MessageChannelMain, utilityProcess, type UtilityProcess, type WebContents } from 'electron'
import { app, MessageChannelMain, utilityProcess, type UtilityProcess, type WebContents } from 'electron'
import { join } from 'node:path'
import { istKernNachricht, type HauptNachricht, type KernNachricht } from '../gemeinsam/nachrichten'
@@ -35,9 +35,16 @@ export class KernVerwaltung {
// Der Smoke-Beweis läuft ohne Netz und ohne MakeMKV — der Kern lässt
// die Schlüsselkette dann aus.
if (this.smokeModus) args.push('--smoke-kern')
// Im installierten Rippy liegen die mitgelieferten Werkzeuge unter
// resources\vendor — der Kern findet sie über RIPPY_VENDOR (§ 9).
const umgebung: NodeJS.ProcessEnv = { ...process.env }
if (app.isPackaged) {
umgebung['RIPPY_VENDOR'] = join(process.resourcesPath, 'vendor')
}
const kern = utilityProcess.fork(kernDatei, args, {
serviceName: 'Rippy-Kern',
stdio: 'inherit',
env: umgebung,
})
this.kern = kern
+20
View File
@@ -0,0 +1,20 @@
// Die reinen Update-Regeln (§ 6.8) — getrennt von update.ts, damit die
// Tests sie ohne Electron laden können.
export const STANDARD_UPDATE_URL =
'https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/rippy/releases/download/aktuell'
/** Prüft die Update-Adresse: NUR https:// ist zulässig (§ 6.8 Regel 1) —
* ohne Code-Signing (Entscheid 5) ist der Transportweg die einzige
* Absicherung; eine http://-Adresse wird ABGELEHNT, nicht still
* akzeptiert. */
export function updateUrlPruefen(url: string): { ok: boolean; grund: string } {
const sauber = url.trim()
if (sauber.length === 0) return { ok: true, grund: '' } // Standard gilt
if (sauber.startsWith('https://')) return { ok: true, grund: '' }
return {
ok: false,
grund:
'Die Update-Adresse muss mit https:// beginnen — ohne Signatur ist der sichere Transportweg die einzige Absicherung (KONZEPT § 6.8).',
}
}
+83
View File
@@ -0,0 +1,83 @@
// Selbst-Update (KONZEPT § 6.8) — electron-updater gegen die Releases im
// öffentlichen Gitea (Entscheid 6). Die vier Regeln:
// 1. NUR HTTPS — ohne Code-Signing (Entscheid 5) ist der Transportweg
// die einzige Absicherung; eine http://-Adresse wird ABGELEHNT.
// 2. NIE während eines Rips — weder laden noch installieren.
// 3. Ein Fehlschlag ist sichtbar, aber nicht laut: Der Zeitpunkt der
// letzten erfolgreichen Prüfung steht im Status.
// 4. Die laufende Version ist ablesbar (Fenster + Protokollkopf).
import { app } from 'electron'
import electronUpdater from 'electron-updater'
import { updateUrlPruefen } from './update-regeln'
export { STANDARD_UPDATE_URL, updateUrlPruefen } from './update-regeln'
const { autoUpdater } = electronUpdater
export interface UpdateStand {
/** Wann die letzte Prüfung GELANG — '' wenn noch nie. */
letzteErfolgreichePruefung: string
text: string
}
const stand: UpdateStand = { letzteErfolgreichePruefung: '', text: 'noch nicht geprüft' }
export function updateStand(): UpdateStand {
return { ...stand }
}
export function updateEinrichten(
eigeneUrl: string,
ripLaeuft: () => boolean,
melden: (text: string) => void,
): void {
if (!app.isPackaged) {
stand.text = 'Entwicklungslauf — Selbst-Update ist nur im installierten Rippy aktiv.'
return
}
const pruefung = updateUrlPruefen(eigeneUrl)
if (!pruefung.ok) {
stand.text = pruefung.grund
melden(pruefung.grund)
return
}
if (eigeneUrl.trim().length > 0) {
autoUpdater.setFeedURL({ provider: 'generic', url: eigeneUrl.trim() })
}
autoUpdater.autoDownload = false
// Installiert wird beim NÄCHSTEN Start — nie mitten in einem Rip.
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', () => {
if (ripLaeuft()) {
// Regel 2: nicht einmal herunterladen, solange gerippt wird — der
// tägliche Takt versucht es wieder.
stand.text = 'Update verfügbar — wird geladen, sobald kein Rip läuft.'
return
}
void autoUpdater.downloadUpdate().catch((fehler: unknown) => {
stand.text = `Update-Download fehlgeschlagen: ${String(fehler)}`
})
})
autoUpdater.on('update-downloaded', (info) => {
stand.text = `Version ${info.version} liegt bereit — sie wird beim nächsten Start installiert.`
melden(stand.text)
})
autoUpdater.on('update-not-available', () => {
stand.letzteErfolgreichePruefung = new Date().toISOString()
stand.text = 'Rippy ist aktuell.'
})
autoUpdater.on('error', (fehler) => {
// Regel 3: still scheitern (WLAN weg, Server aus) — aber ablesbar.
stand.text = `Letzte Prüfung scheiterte: ${String(fehler).slice(0, 160)}`
})
const pruefen = (): void => {
if (ripLaeuft()) return
void autoUpdater.checkForUpdates().catch(() => {
// der error-Handler oben trägt den Grund ein
})
}
pruefen()
setInterval(pruefen, 24 * 60 * 60 * 1000).unref?.()
}
+7 -1
View File
@@ -261,6 +261,8 @@ const ERLAUBTE_EINSTELLUNGEN = new Set([
'tmdbApiKey',
'omdbApiKey',
'makemkvAppKey',
'updateUrl',
'setup.done',
'autoAuswurf',
'werkzeug.makemkv',
'werkzeug.handbrake',
@@ -273,7 +275,9 @@ function einstellungenSenden(): void {
const wert = db?.einstellungLesen(schluessel)
if (wert !== null && wert !== undefined) werte[schluessel] = wert
}
anFenster({ art: 'einstellungen', werte })
// AUCH ans Haupt: Es braucht die Update-Adresse (§ 6.8) — die eine
// Einstellungs-Quelle bleibt trotzdem der Kern.
anHauptUndFenster({ art: 'einstellungen', werte })
}
// ── MakeMKV-Schlüsselkette (§ 9.1): beim Start und täglich ──────────────
@@ -400,6 +404,8 @@ function bereitNachricht(): KernNachricht {
}
process.parentPort.postMessage(bereitNachricht())
// Das Haupt braucht die Einstellungen (Update-Adresse) auch ohne Fenster.
einstellungenSenden()
// Nicht im Smoke: Der Schlüssel-Lauf spricht Forum + makemkvcon an —
// der Gerüst-Beweis soll ohne Netz und ohne MakeMKV grün sein.
if (!process.argv.includes('--smoke-kern') && process.env['RIPPY_OHNE_SCHLUESSEL'] !== '1') {
@@ -92,6 +92,14 @@ export function kandidaten(
): string[] {
const werkzeug = WERKZEUGE[name]
const pfade: string[] = []
// Das MITGELIEFERTE Werkzeug (§ 9) — im installierten Rippy zeigt
// RIPPY_VENDOR auf resources\vendor (der Haupt setzt es beim Kern-Start).
const vendor = umgebung['RIPPY_VENDOR'] ?? ''
if (vendor.length > 0) {
for (const programm of werkzeug.programme) {
pfade.push(join(vendor, name, programm))
}
}
for (const programm of werkzeug.programme) {
pfade.push(join(eigenerOrdner, name, programm))
pfade.push(join(eigenerOrdner, programm))