hmgr 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/claude-code.js +21 -17
- package/dist/adapter/dateisystem.js +105 -5
- package/dist/adapter/git.js +96 -10
- package/dist/adapter/hermes.js +163 -10
- package/dist/adapter/skill-md.js +94 -18
- package/dist/adapter/terminal.js +1 -1
- package/dist/adapter/zustand-json.js +4 -0
- package/dist/cli/hilfe.js +129 -26
- package/dist/cli/main.js +268 -20
- package/dist/gui/icons.js +114 -0
- package/dist/gui/main.js +481 -0
- package/dist/gui/oeffnen.js +33 -0
- package/dist/gui/preload.cjs +55 -0
- package/dist/gui/renderer/fonts/Inter-LICENSE.txt +92 -0
- package/dist/gui/renderer/fonts/InterVariable.woff2 +0 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-Medium.woff2 +0 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-OFL.txt +93 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/dist/gui/renderer/index.html +491 -0
- package/dist/gui/renderer/renderer.js +2611 -0
- package/dist/gui/renderer/styles.css +638 -0
- package/dist/kern/anwendungsfaelle.js +688 -65
- package/dist/kern/modell.js +159 -0
- package/dist/ports/harness.js +13 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +5 -2
package/dist/gui/main.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// hmgr-GUI, Electron-Main (E40). Baut dieselben Anwendungsfälle wie die CLI (src/cli/main.ts),
|
|
2
|
+
// registriert IPC-Handler für den Renderer und implementiert den Rückfrage-Port als Dialog im
|
|
3
|
+
// Fenster. Kein zweiter Prozess: der Kern läuft direkt im Main-Prozess, die GUI ist ein zweiter
|
|
4
|
+
// Kompositions-Einstieg neben der CLI (ARCHITEKTUR.md „Eingang").
|
|
5
|
+
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { stat } from 'node:fs/promises';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
12
|
+
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
13
|
+
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
14
|
+
import { iconFuerQuelle } from "./icons.js";
|
|
15
|
+
import { entscheideOeffnen } from "./oeffnen.js";
|
|
16
|
+
const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
|
|
18
|
+
const umgebung = umgebungLesen();
|
|
19
|
+
/** Cache der Quellen-Icons (E60), unterhalb von HMGR_HOME wie der Zustand: Tests/Smoke fassen so
|
|
20
|
+
nie ein echtes Verzeichnis an. */
|
|
21
|
+
const iconCacheVerzeichnis = path.join(umgebung.hmgrHome, 'icon-cache');
|
|
22
|
+
/** Intervall des selbsttätigen Aktualisierens (E44): fest, keine Einstellung. */
|
|
23
|
+
const SECHS_STUNDEN_MS = 6 * 60 * 60 * 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Fake der claude-CLI für den Smoke-Test (E40 R4), nach dem Muster von `fakeClaude` in
|
|
26
|
+
* tests/helfer.ts: nie `code: 0`, also ohne Plugin-Fähigkeit, wie ohne installierte CLI. So fasst
|
|
27
|
+
* ein automatisierter Lauf formal nie die echte claude-CLI an, auch wenn sie auf dem PATH liegt.
|
|
28
|
+
*/
|
|
29
|
+
const smoketestClaudeAusfuehrer = async () => ({ stdout: '', code: 1 });
|
|
30
|
+
let hauptfenster = null;
|
|
31
|
+
/**
|
|
32
|
+
* Rückfrage-Port als Dialog im Renderer (E28, E34): der Main-Prozess schickt die Frage per
|
|
33
|
+
* `webContents.send`, der Renderer zeigt einen <dialog> im Stil des Entwurfs und schickt die
|
|
34
|
+
* Antwort über einen Kanal mit Korrelations-ID zurück. Ohne offenes Fenster kann niemand antworten,
|
|
35
|
+
* dort gilt wie am Terminal ohne TTY: Nein bzw. keine Antwort (E34).
|
|
36
|
+
*/
|
|
37
|
+
class GuiRueckfrage {
|
|
38
|
+
ausstehend = new Map();
|
|
39
|
+
constructor() {
|
|
40
|
+
ipcMain.on('hmgr:rueckfrage:antwort', (_event, nachricht) => {
|
|
41
|
+
const aufloesen = this.ausstehend.get(nachricht.id);
|
|
42
|
+
if (aufloesen === undefined)
|
|
43
|
+
return;
|
|
44
|
+
this.ausstehend.delete(nachricht.id);
|
|
45
|
+
aufloesen(nachricht.antwort);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
stellen(payload) {
|
|
49
|
+
if (hauptfenster === null)
|
|
50
|
+
return Promise.resolve(payload.art === 'bestaetigen' ? false : undefined);
|
|
51
|
+
const id = randomUUID();
|
|
52
|
+
return new Promise((aufloesen) => {
|
|
53
|
+
this.ausstehend.set(id, aufloesen);
|
|
54
|
+
hauptfenster?.webContents.send('hmgr:rueckfrage', { id, ...payload });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async bestaetigen(frage) {
|
|
58
|
+
const antwort = await this.stellen({ art: 'bestaetigen', frage });
|
|
59
|
+
return antwort === true;
|
|
60
|
+
}
|
|
61
|
+
async auswaehlen(frage, optionen) {
|
|
62
|
+
const antwort = await this.stellen({ art: 'auswaehlen', frage, optionen });
|
|
63
|
+
return typeof antwort === 'number' ? antwort : undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rueckfrage = new GuiRueckfrage();
|
|
67
|
+
const { anwendungsfaelle } = bauen(umgebung, {
|
|
68
|
+
rueckfrage,
|
|
69
|
+
...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
|
|
70
|
+
});
|
|
71
|
+
async function auffangen(fn) {
|
|
72
|
+
try {
|
|
73
|
+
return { ok: true, daten: await fn() };
|
|
74
|
+
}
|
|
75
|
+
catch (fehler) {
|
|
76
|
+
return { ok: false, fehler: fehler instanceof Error ? fehler.message : String(fehler) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** ZielZustand über IPC: Maps werden zu einfachen Objekten, der Rest ist schon JSON-tauglich. */
|
|
80
|
+
function zielZustandSerialisieren(z) {
|
|
81
|
+
return {
|
|
82
|
+
ziel: z.ziel,
|
|
83
|
+
verzeichnis: z.verzeichnis,
|
|
84
|
+
eintraege: z.eintraege,
|
|
85
|
+
einschraenkungen: Object.fromEntries(z.einschraenkungen),
|
|
86
|
+
gruende: Object.fromEntries(z.gruende),
|
|
87
|
+
ignoriert: z.ignoriert,
|
|
88
|
+
uebernahme: Object.fromEntries(z.uebernahme),
|
|
89
|
+
istRegistriert: z.istRegistriert,
|
|
90
|
+
kannSkillOrdner: z.kannSkillOrdner,
|
|
91
|
+
...(z.plugins === undefined ? {} : { plugins: z.plugins }),
|
|
92
|
+
...(z.verwalteterSkillOrdner === undefined ? {} : { verwalteterSkillOrdner: z.verwalteterSkillOrdner }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function zielAusIpc(harness, ort) {
|
|
96
|
+
return { harness, ort };
|
|
97
|
+
}
|
|
98
|
+
function registriereIpc() {
|
|
99
|
+
ipcMain.handle('hmgr:version', async () => auffangen(async () => app.getVersion()));
|
|
100
|
+
// Für pfadKurz im Renderer (Anzeige-Kürzung auf ~): nur lesend, nie in einen anderen Aufruf zurückgespeist.
|
|
101
|
+
ipcMain.handle('hmgr:home', async () => auffangen(async () => os.homedir()));
|
|
102
|
+
ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand()).map(zielZustandSerialisieren)));
|
|
103
|
+
ipcMain.handle('hmgr:marketplaces', async () => auffangen(async () => (await anwendungsfaelle.marketplaces()).map((m) => ({
|
|
104
|
+
quelleText: quelleAlsText(m.quelle),
|
|
105
|
+
name: m.name,
|
|
106
|
+
plugins: m.plugins,
|
|
107
|
+
}))));
|
|
108
|
+
// Skill-Katalog über alle bekannten Quellen (E40 R3): über den Anwendungsfall `katalog`, nicht
|
|
109
|
+
// mehr direkt über die Ports. Eine nicht erreichbare Quelle trägt einen Fehlervermerk (E12-Muster
|
|
110
|
+
// wie istZustand/marketplaces), ihre Skills fehlen einfach in der flachen Liste.
|
|
111
|
+
ipcMain.handle('hmgr:skills', async () => auffangen(async () => {
|
|
112
|
+
const ergebnis = [];
|
|
113
|
+
for (const eintrag of await anwendungsfaelle.katalog()) {
|
|
114
|
+
if (eintrag.ref === undefined)
|
|
115
|
+
continue; // nicht erreichbar, keine Skills
|
|
116
|
+
for (const skill of eintrag.skills)
|
|
117
|
+
ergebnis.push({
|
|
118
|
+
name: skill.name,
|
|
119
|
+
pfad: skill.pfad,
|
|
120
|
+
...(skill.beschreibung === undefined ? {} : { beschreibung: skill.beschreibung }),
|
|
121
|
+
...(skill.nurPerBefehl === undefined ? {} : { nurPerBefehl: skill.nurPerBefehl }),
|
|
122
|
+
...(skill.nichtPerBefehl === undefined ? {} : { nichtPerBefehl: skill.nichtPerBefehl }),
|
|
123
|
+
...(skill.registriertHooks === undefined ? {} : { registriertHooks: skill.registriertHooks }),
|
|
124
|
+
...(skill.werkzeugeErlaubt === undefined ? {} : { werkzeugeErlaubt: skill.werkzeugeErlaubt }),
|
|
125
|
+
...(skill.kompatibilitaet === undefined ? {} : { kompatibilitaet: skill.kompatibilitaet }),
|
|
126
|
+
...(skill.modell === undefined ? {} : { modell: skill.modell }),
|
|
127
|
+
...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
|
|
128
|
+
...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
|
|
129
|
+
quelleText: quelleAlsText(eintrag.quelle),
|
|
130
|
+
quelleName: quelleName(eintrag.quelle),
|
|
131
|
+
quelleSchluessel: quelleSchluessel(eintrag.quelle),
|
|
132
|
+
ref: eintrag.ref,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return ergebnis;
|
|
136
|
+
}));
|
|
137
|
+
// Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
|
|
138
|
+
ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
|
|
139
|
+
art: quelle.art,
|
|
140
|
+
text: quelleAlsText(quelle),
|
|
141
|
+
name: quelleName(quelle),
|
|
142
|
+
}))));
|
|
143
|
+
// Quelle-Verwaltung in den Einstellungen (E54, E59): Text-zu-Quelle passiert im Main-Prozess, der
|
|
144
|
+
// Renderer kennt nur Zeichenketten. Der Anzeigename kommt serverseitig mit, damit dem Renderer nie
|
|
145
|
+
// ein weggefiltertes Feld fehlt (E59: „name" ist reine Anzeige, geht nicht in quelleAusText ein).
|
|
146
|
+
ipcMain.handle('hmgr:quelleHinzufuegen', async (_event, text, name) => auffangen(async () => {
|
|
147
|
+
const ergebnis = await anwendungsfaelle.quelleHinzufuegen(quelleAusText(text), name);
|
|
148
|
+
return {
|
|
149
|
+
quelle: {
|
|
150
|
+
art: ergebnis.quelle.art,
|
|
151
|
+
text: quelleAlsText(ergebnis.quelle),
|
|
152
|
+
name: quelleName(ergebnis.quelle),
|
|
153
|
+
},
|
|
154
|
+
skills: ergebnis.skills,
|
|
155
|
+
marketplace: ergebnis.marketplace,
|
|
156
|
+
bekannt: ergebnis.bekannt,
|
|
157
|
+
};
|
|
158
|
+
}));
|
|
159
|
+
ipcMain.handle('hmgr:quelleEntfernen', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleEntfernen(quelleAusText(text))));
|
|
160
|
+
ipcMain.handle('hmgr:quelleUmbenennen', async (_event, text, name) => auffangen(async () => {
|
|
161
|
+
const umbenannt = await anwendungsfaelle.quelleUmbenennen(quelleAusText(text), name);
|
|
162
|
+
return { art: umbenannt.art, text: quelleAlsText(umbenannt), name: quelleName(umbenannt) };
|
|
163
|
+
}));
|
|
164
|
+
// Aufräum-Angebot nach dem Entfernen einer Quelle (E58): Inventar vor dem eigentlichen Entfernen
|
|
165
|
+
// erheben (der Renderer ruft das, bevor er quelleEntfernen aufruft), dann bei Zustimmung abräumen.
|
|
166
|
+
// Beide Ergebnisse reichen unverändert durch: schon reine Daten, keine Maps oder Funktionen darin.
|
|
167
|
+
ipcMain.handle('hmgr:quelleAufraeumInventar', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleAufraeumInventar(quelleAusText(text))));
|
|
168
|
+
ipcMain.handle('hmgr:quelleAufraeumen', async (_event, inventar) => auffangen(async () => anwendungsfaelle.quelleAufraeumen(inventar)));
|
|
169
|
+
// Die GUI arbeitet an einem Arbeitsverzeichnis wie die CLI mit --local (E57, F32): Links werden
|
|
170
|
+
// lokal gelegt und entfernt, der Zeiger bleibt unangetastet, der Kern hängt bei einer bestehenden
|
|
171
|
+
// Bindung von sich aus einen Hinweis an. Für Home und Profil ist `lokal` folgenlos (E26).
|
|
172
|
+
ipcMain.handle('hmgr:linkLegen', async (_event, args) => auffangen(async () => anwendungsfaelle.linkLegen({
|
|
173
|
+
skill: args.skill,
|
|
174
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
175
|
+
...(args.quelle === undefined ? {} : { quelle: args.quelle }),
|
|
176
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
177
|
+
})));
|
|
178
|
+
ipcMain.handle('hmgr:linkEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.linkEntfernen({
|
|
179
|
+
skill: args.skill,
|
|
180
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
181
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
182
|
+
})));
|
|
183
|
+
ipcMain.handle('hmgr:eintragIgnorieren', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragIgnorieren({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
184
|
+
ipcMain.handle('hmgr:eintragBeachten', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragBeachten({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
185
|
+
// Auch die Übernahme legt Links und schriebe ohne `lokal` den Zeiger (E31 über linkAusFund), sie
|
|
186
|
+
// trifft also dieselbe Sperre wie link (E57). In der GUI arbeitet sie darum wie die CLI mit
|
|
187
|
+
// --local (F32), sonst bräche der Knopf an jedem gebundenen Arbeitsverzeichnis ab.
|
|
188
|
+
ipcMain.handle('hmgr:uebernehmen', async (_event, args) => auffangen(async () => anwendungsfaelle.uebernehmen({
|
|
189
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
190
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
191
|
+
probelauf: args.probelauf,
|
|
192
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
193
|
+
})));
|
|
194
|
+
ipcMain.handle('hmgr:eintragLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragLoeschen({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
195
|
+
ipcMain.handle('hmgr:aktualisieren', async (_event, args) => auffangen(async () => anwendungsfaelle.aktualisieren(args)));
|
|
196
|
+
ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
|
|
197
|
+
ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
|
|
198
|
+
// Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). Im Smoke-Test darf
|
|
199
|
+
// nie ein echter Systemdialog aufgehen: sofort null, wie bei Abbruch.
|
|
200
|
+
ipcMain.handle('hmgr:verzeichnisWaehlen', async () => auffangen(async () => {
|
|
201
|
+
if (smoketest || hauptfenster === null)
|
|
202
|
+
return null;
|
|
203
|
+
const ergebnis = await dialog.showOpenDialog(hauptfenster, {
|
|
204
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
205
|
+
});
|
|
206
|
+
if (ergebnis.canceled || ergebnis.filePaths.length === 0)
|
|
207
|
+
return null;
|
|
208
|
+
return ergebnis.filePaths[0] ?? null;
|
|
209
|
+
}));
|
|
210
|
+
// Harness-Schalter im Einstellungen-Modal (E38, GRILL.md Entwurfskritik 4).
|
|
211
|
+
ipcMain.handle('hmgr:harnessTypen', async () => auffangen(async () => anwendungsfaelle.harnessTypen()));
|
|
212
|
+
ipcMain.handle('hmgr:aktiveHarnesses', async () => auffangen(async () => anwendungsfaelle.aktiveHarnesses()));
|
|
213
|
+
// Für den Vermerk „nicht installiert" an den Schaltern (E42).
|
|
214
|
+
ipcMain.handle('hmgr:nichtVorhandeneHarnesses', async () => auffangen(async () => anwendungsfaelle.nichtVorhandeneHarnesses()));
|
|
215
|
+
ipcMain.handle('hmgr:harnessAktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessAktivieren(name)));
|
|
216
|
+
ipcMain.handle('hmgr:harnessDeaktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessDeaktivieren(name)));
|
|
217
|
+
ipcMain.handle('hmgr:saeubern', async (_event, name) => auffangen(async () => anwendungsfaelle.saeubern(name)));
|
|
218
|
+
// Verwalteter Hermes-Skill-Ordner (E56): Zeigen kommt schon über hmgr:istZustand, hier nur Ändern.
|
|
219
|
+
ipcMain.handle('hmgr:skillOrdnerFestlegen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerFestlegen(zielAusIpc(args.harness, args.ort), args.pfad)));
|
|
220
|
+
ipcMain.handle('hmgr:skillOrdnerEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerEntfernen(zielAusIpc(args.harness, args.ort))));
|
|
221
|
+
ipcMain.handle('hmgr:skillOrdnerLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerLoeschen(zielAusIpc(args.harness, args.ort), args.pfad)));
|
|
222
|
+
// Quellen-Icon (E60): nur im Main-Prozess geladen, als data-URL geliefert (CSP erlaubt img-src
|
|
223
|
+
// data:). Im Smoke-Test nie ein Netzzugriff: sofort null, der Renderer zeigt dann das generische
|
|
224
|
+
// Symbol. Ein Fehlschlag wirft nie (auffangen fängt ihn, iconFuerQuelle selbst wirft ohnehin nicht).
|
|
225
|
+
ipcMain.handle('hmgr:icon', async (_event, quelleText) => auffangen(async () => {
|
|
226
|
+
if (smoketest)
|
|
227
|
+
return null;
|
|
228
|
+
return iconFuerQuelle(quelleAusText(quelleText), { cacheVerzeichnis: iconCacheVerzeichnis });
|
|
229
|
+
}));
|
|
230
|
+
// Quelle öffnen (E60): die Protokoll-Weiche steht in oeffnen.ts (rein, testbar), hier nur die
|
|
231
|
+
// Wirkung. `shell.openExternal` bekommt ausschließlich http(s)-URLs, `shell.openPath` nur einen
|
|
232
|
+
// Ordner, den es wirklich gibt — nichts anderes wird zugelassen. Alles, was es gibt, aber kein
|
|
233
|
+
// Ordner ist, wird im Finder nur gezeigt, nicht geöffnet: `openPath` startet sonst eine Datei
|
|
234
|
+
// mit ihrem Standardprogramm, und ein Pfad aus dem Renderer soll nichts starten können.
|
|
235
|
+
ipcMain.handle('hmgr:quelleOeffnen', async (_event, quelleText) => auffangen(async () => {
|
|
236
|
+
const entscheidung = entscheideOeffnen(quelleAusText(quelleText));
|
|
237
|
+
if (entscheidung.art === 'browser')
|
|
238
|
+
await shell.openExternal(entscheidung.url);
|
|
239
|
+
else if (entscheidung.art === 'ordner') {
|
|
240
|
+
const info = await stat(entscheidung.pfad).catch(() => undefined);
|
|
241
|
+
if (info === undefined)
|
|
242
|
+
throw new Error(`Es gibt „${entscheidung.pfad}" nicht (mehr).`);
|
|
243
|
+
if (info.isDirectory())
|
|
244
|
+
await shell.openPath(entscheidung.pfad);
|
|
245
|
+
else
|
|
246
|
+
shell.showItemInFolder(entscheidung.pfad);
|
|
247
|
+
}
|
|
248
|
+
return entscheidung;
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
function fensterOeffnen() {
|
|
252
|
+
hauptfenster = new BrowserWindow({
|
|
253
|
+
width: 1280,
|
|
254
|
+
height: 860,
|
|
255
|
+
minWidth: 860,
|
|
256
|
+
minHeight: 560,
|
|
257
|
+
backgroundColor: '#E3E7E0', // --platte, verhindert einen weißen Blitz beim Start
|
|
258
|
+
// Im Smoke-Test bleibt das Fenster unsichtbar: der Renderer lädt trotzdem vollständig, der
|
|
259
|
+
// DOM-Auszug kommt per executeJavaScript. So klaut ein automatisierter Lauf niemandem den
|
|
260
|
+
// Eingabefokus und öffnet kein sichtbares Fenster.
|
|
261
|
+
show: !smoketest,
|
|
262
|
+
webPreferences: {
|
|
263
|
+
preload: path.join(hierVerzeichnis, 'preload.cjs'),
|
|
264
|
+
contextIsolation: true,
|
|
265
|
+
nodeIntegration: false,
|
|
266
|
+
sandbox: true,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
hauptfenster.on('closed', () => {
|
|
270
|
+
hauptfenster = null;
|
|
271
|
+
});
|
|
272
|
+
// Renderer-Konsole im Main-Log, für Fehlersuche und den Smoke-Test.
|
|
273
|
+
hauptfenster.webContents.on('console-message', (event) => {
|
|
274
|
+
console.log(`[renderer] ${event.message}`);
|
|
275
|
+
});
|
|
276
|
+
// Nur für den Smoke-Test (scripts/gui-smoke-test.ts): ein DOM-Auszug im Log, danach Ende.
|
|
277
|
+
if (smoketest) {
|
|
278
|
+
const fenster = hauptfenster;
|
|
279
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
void fenster.webContents
|
|
282
|
+
.executeJavaScript(`(async () => {
|
|
283
|
+
// Einstellungen-Modal (E40 R2): öffnen, die Harness-Schalter ablesen, wieder schließen.
|
|
284
|
+
document.getElementById('einstellungen')?.click();
|
|
285
|
+
var einstellungenOffen = document.getElementById('d-einstellungen')?.open === true;
|
|
286
|
+
var harnessSchalter = Array.from(document.querySelectorAll('#harnessliste input[type=checkbox]'))
|
|
287
|
+
.map((i) => i.dataset.harness + ':' + i.checked)
|
|
288
|
+
.join(',');
|
|
289
|
+
// Quellen-Abschnitt der Einstellungen (E54): Knopf und Dialoge vorhanden, der
|
|
290
|
+
// Hinzufügen-Dialog lässt sich öffnen und per Abbrechen ohne Fehler wieder schließen.
|
|
291
|
+
// Kein echter Quellen-Lauf: der Abbrechen-Knopf löst keinen IPC-Aufruf aus.
|
|
292
|
+
var quelleneuVorhanden = document.getElementById('quelleneu') !== null;
|
|
293
|
+
var dialogeQuelleVorhanden =
|
|
294
|
+
document.getElementById('d-quelle') !== null && document.getElementById('d-quelle-vergessen') !== null;
|
|
295
|
+
var quelleFehler = null;
|
|
296
|
+
var dQuelleOffenNachOeffnen = false;
|
|
297
|
+
var dQuelleOffenNachAbbrechen = true;
|
|
298
|
+
try {
|
|
299
|
+
document.getElementById('quelleneu')?.click();
|
|
300
|
+
dQuelleOffenNachOeffnen = document.getElementById('d-quelle')?.open === true;
|
|
301
|
+
document.querySelector('#d-quelle button[value="abbruch"]')?.click();
|
|
302
|
+
dQuelleOffenNachAbbrechen = document.getElementById('d-quelle')?.open === true;
|
|
303
|
+
} catch (fehler) {
|
|
304
|
+
quelleFehler = String(fehler);
|
|
305
|
+
}
|
|
306
|
+
// Aufräum-Angebot nach Quelle entfernen (E58): kein echter Lauf (der würde Checkouts
|
|
307
|
+
// und Links wirklich löschen), nur ob der Dialog existiert und sich öffnen und über
|
|
308
|
+
// Behalten schließen lässt, ohne Fehler.
|
|
309
|
+
var dialogQuelleAufraeumenVorhanden = document.getElementById('d-quelle-aufraeumen') !== null;
|
|
310
|
+
var quelleAufraeumenFehler = null;
|
|
311
|
+
var dQuelleAufraeumenOffenNachOeffnen = false;
|
|
312
|
+
var dQuelleAufraeumenOffenNachBehalten = true;
|
|
313
|
+
try {
|
|
314
|
+
var dQuelleAufraeumen = document.getElementById('d-quelle-aufraeumen');
|
|
315
|
+
dQuelleAufraeumen?.showModal();
|
|
316
|
+
dQuelleAufraeumenOffenNachOeffnen = dQuelleAufraeumen?.open === true;
|
|
317
|
+
document.querySelector('#d-quelle-aufraeumen button[value="behalten"]')?.click();
|
|
318
|
+
dQuelleAufraeumenOffenNachBehalten = dQuelleAufraeumen?.open === true;
|
|
319
|
+
} catch (fehler) {
|
|
320
|
+
quelleAufraeumenFehler = String(fehler);
|
|
321
|
+
}
|
|
322
|
+
// Anzeigename der Quelle (E59): die Einstellungen-Liste zeigt den abgeleiteten Namen
|
|
323
|
+
// „skills-repo" statt der vollen Fixture-URL, das Namensfeld im Hinzufügen-Dialog gibt
|
|
324
|
+
// es zusätzlich zum URL/Pfad-Feld.
|
|
325
|
+
var quellenlisteNamen = Array.from(document.querySelectorAll('#quellenliste b'))
|
|
326
|
+
.map((b) => b.textContent)
|
|
327
|
+
.join(',');
|
|
328
|
+
var dqNameVorhanden = document.getElementById('dq-name') !== null;
|
|
329
|
+
// Umbenennen (E59): der Knopf je Quelle öffnet den Dialog mit dem aktuellen Namen im
|
|
330
|
+
// Feld, Abbrechen schließt ihn wieder. Beide Knöpfe der Zeile liegen zusammen in
|
|
331
|
+
// einem Kasten, sonst lägen sie im Grid übereinander.
|
|
332
|
+
var quellenknoepfeZahl = document.querySelectorAll('#quellenliste .quellenknoepfe').length;
|
|
333
|
+
var quelleUmbenennenFehler = null;
|
|
334
|
+
var dQuUOffenNachOeffnen = false;
|
|
335
|
+
var dQuUName = null;
|
|
336
|
+
var dQuUOffenNachAbbrechen = true;
|
|
337
|
+
try {
|
|
338
|
+
var dQuU = document.getElementById('d-quelle-umbenennen');
|
|
339
|
+
// E60: der Umbenennen-Knopf ist jetzt ein Stift-Icon-Knopf ohne Text (Klasse
|
|
340
|
+
// „umbenennen", aria-label statt Beschriftung).
|
|
341
|
+
document.querySelector('#quellenliste .umbenennen')?.click();
|
|
342
|
+
dQuUOffenNachOeffnen = dQuU?.open === true;
|
|
343
|
+
dQuUName = document.getElementById('dqu-name')?.value ?? null;
|
|
344
|
+
document.querySelector('#d-quelle-umbenennen button[value="abbruch"]')?.click();
|
|
345
|
+
dQuUOffenNachAbbrechen = dQuU?.open === true;
|
|
346
|
+
} catch (fehler) {
|
|
347
|
+
quelleUmbenennenFehler = String(fehler);
|
|
348
|
+
}
|
|
349
|
+
document.getElementById('d-einstellungen')?.close();
|
|
350
|
+
// Konfigzeile am Hermes-Ziel (E56): der verwaltete Pfad steht in der Karte, auch
|
|
351
|
+
// ungeklappt (hidden ändert textContent nicht). Dazu der Festlegen-Weg am unmanaged
|
|
352
|
+
// Profil, Öffnen und Abbrechen ohne Fehler, nach dem Muster der d-quelle-Prüfungen.
|
|
353
|
+
var konfigzeileEl = document.querySelector('.konfigzeile');
|
|
354
|
+
var konfigzeileText = konfigzeileEl ? konfigzeileEl.textContent.replace(/\\s+/g, ' ').trim() : null;
|
|
355
|
+
var skillOrdnerFehler = null;
|
|
356
|
+
var dSkillordnerOffenNachOeffnen = false;
|
|
357
|
+
var dSkillordnerOffenNachAbbrechen = true;
|
|
358
|
+
try {
|
|
359
|
+
var festlegenKnopf = Array.from(document.querySelectorAll('button'))
|
|
360
|
+
.find((b) => b.textContent === 'Skill-Ordner festlegen …');
|
|
361
|
+
festlegenKnopf?.click();
|
|
362
|
+
dSkillordnerOffenNachOeffnen = document.getElementById('d-skillordner')?.open === true;
|
|
363
|
+
document.querySelector('#d-skillordner button[value="abbruch"]')?.click();
|
|
364
|
+
dSkillordnerOffenNachAbbrechen = document.getElementById('d-skillordner')?.open === true;
|
|
365
|
+
} catch (fehler) {
|
|
366
|
+
skillOrdnerFehler = String(fehler);
|
|
367
|
+
}
|
|
368
|
+
// E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
|
|
369
|
+
// Zahnrad statt Sonne am Einstellungen-Knopf.
|
|
370
|
+
var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
|
|
371
|
+
var zielManuellVorhanden = (document.getElementById('zielliste')?.textContent ?? '').includes('manuell');
|
|
372
|
+
var zahnradVorhanden = (document.querySelector('#einstellungen svg')?.innerHTML ?? '').includes('6.47 3.56');
|
|
373
|
+
// Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
|
|
374
|
+
document.querySelector('[data-ansicht="katalog"]')?.click();
|
|
375
|
+
var katalogBeschreibung = document.querySelector('#katalog .zeile .beschreibung')?.textContent ?? null;
|
|
376
|
+
// Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
|
|
377
|
+
// einem Klick auf den Aufklapp-Knopf.
|
|
378
|
+
var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
|
|
379
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
380
|
+
var aufklapp = document.querySelector('#katalog .zeile .aufklappknopf');
|
|
381
|
+
var katalogDetailsZu = document.querySelector('#katalog .zeile .skillkoerper')?.hidden ?? null;
|
|
382
|
+
aufklapp?.click();
|
|
383
|
+
var katalogAufgeklappt = aufklapp?.getAttribute('aria-expanded') ?? null;
|
|
384
|
+
var katalogDetails = document.querySelector('#katalog .zeile .skillkoerper:not([hidden])')?.textContent
|
|
385
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
386
|
+
// Sicherheitsrelevante Marken (F11): Hooks und Werkzeuge tragen die warnende Klasse.
|
|
387
|
+
var katalogMarkenWarnend = Array.from(document.querySelectorAll('#katalog .zeile .merker.warnend'))
|
|
388
|
+
.map((m) => m.textContent).join(',');
|
|
389
|
+
// Skills-hinzufügen-Dialog (F13): dieselben Marken an der Wahlzeile.
|
|
390
|
+
var dskKnopf = Array.from(document.querySelectorAll('button'))
|
|
391
|
+
.find((b) => b.textContent === 'Skills hinzufügen');
|
|
392
|
+
var dskMarken = null;
|
|
393
|
+
if (dskKnopf) {
|
|
394
|
+
dskKnopf.click();
|
|
395
|
+
dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
|
|
396
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
397
|
+
document.getElementById('d-skills')?.close();
|
|
398
|
+
}
|
|
399
|
+
// Schriften aus fonts/ (Zusatzpunkt 6): erzwingt das Laden und belegt es danach.
|
|
400
|
+
await Promise.all([
|
|
401
|
+
document.fonts.load('400 16px Inter'),
|
|
402
|
+
document.fonts.load('500 16px "JetBrains Mono"'),
|
|
403
|
+
]);
|
|
404
|
+
return JSON.stringify({
|
|
405
|
+
titel: document.title,
|
|
406
|
+
kopfzahlen: document.getElementById('kopfzahlen')?.textContent,
|
|
407
|
+
zielliste: document.getElementById('zielliste')?.textContent?.replace(/\\s+/g, ' ').trim().slice(0, 600),
|
|
408
|
+
meldung: document.getElementById('meldungtext')?.textContent,
|
|
409
|
+
einstellungenOffen,
|
|
410
|
+
harnessSchalter,
|
|
411
|
+
zielBeschreibung,
|
|
412
|
+
zielManuellVorhanden,
|
|
413
|
+
zahnradVorhanden,
|
|
414
|
+
katalogBeschreibung,
|
|
415
|
+
katalogMarken,
|
|
416
|
+
katalogDetailsZu,
|
|
417
|
+
katalogAufgeklappt,
|
|
418
|
+
katalogDetails,
|
|
419
|
+
katalogMarkenWarnend,
|
|
420
|
+
dskMarken,
|
|
421
|
+
interGeladen: document.fonts.check('400 16px Inter'),
|
|
422
|
+
monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
|
|
423
|
+
quelleneuVorhanden,
|
|
424
|
+
dialogeQuelleVorhanden,
|
|
425
|
+
quellenlisteNamen,
|
|
426
|
+
dqNameVorhanden,
|
|
427
|
+
quellenknoepfeZahl,
|
|
428
|
+
dQuUOffenNachOeffnen,
|
|
429
|
+
dQuUName,
|
|
430
|
+
dQuUOffenNachAbbrechen,
|
|
431
|
+
quelleUmbenennenFehler,
|
|
432
|
+
dQuelleOffenNachOeffnen,
|
|
433
|
+
dQuelleOffenNachAbbrechen,
|
|
434
|
+
quelleFehler,
|
|
435
|
+
dialogQuelleAufraeumenVorhanden,
|
|
436
|
+
dQuelleAufraeumenOffenNachOeffnen,
|
|
437
|
+
dQuelleAufraeumenOffenNachBehalten,
|
|
438
|
+
quelleAufraeumenFehler,
|
|
439
|
+
// Hinweiszeile im Erfolgsdialog (E26, F32): vorhanden und im Ruhezustand leer.
|
|
440
|
+
deHinweisVorhanden: document.getElementById('de-hinweis') !== null,
|
|
441
|
+
deHinweisVerborgen: document.getElementById('de-hinweis')?.hidden === true,
|
|
442
|
+
konfigzeileText,
|
|
443
|
+
dSkillordnerOffenNachOeffnen,
|
|
444
|
+
dSkillordnerOffenNachAbbrechen,
|
|
445
|
+
skillOrdnerFehler,
|
|
446
|
+
});
|
|
447
|
+
})()`)
|
|
448
|
+
.then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
|
|
449
|
+
.finally(() => app.quit());
|
|
450
|
+
}, 3000);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Selbsttätiges Aktualisieren (E44): einmal, sobald der Renderer bereit ist, danach alle sechs
|
|
454
|
+
// Stunden, solange das Fenster offen bleibt. Im Smoke-Test nie, sonst würde ein automatisierter
|
|
455
|
+
// Lauf einen echten Git-Fetch anstoßen.
|
|
456
|
+
if (!smoketest) {
|
|
457
|
+
const fenster = hauptfenster;
|
|
458
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
459
|
+
fenster.webContents.send('hmgr:aktualisierenAnstossen');
|
|
460
|
+
});
|
|
461
|
+
const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
|
|
462
|
+
fenster.on('closed', () => clearInterval(intervall));
|
|
463
|
+
}
|
|
464
|
+
void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
|
|
465
|
+
}
|
|
466
|
+
registriereIpc();
|
|
467
|
+
app.whenReady().then(() => {
|
|
468
|
+
// Im Smoke-Test kein Dock-Icon und kein Fokuswechsel: das Fenster bleibt unsichtbar (show:false
|
|
469
|
+
// oben), macOS würde aber ohne diese Zeile trotzdem ein Dock-Icon zeigen und aktivieren.
|
|
470
|
+
if (smoketest && process.platform === 'darwin')
|
|
471
|
+
app.dock?.hide();
|
|
472
|
+
fensterOeffnen();
|
|
473
|
+
app.on('activate', () => {
|
|
474
|
+
if (BrowserWindow.getAllWindows().length === 0)
|
|
475
|
+
fensterOeffnen();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
app.on('window-all-closed', () => {
|
|
479
|
+
if (process.platform !== 'darwin')
|
|
480
|
+
app.quit();
|
|
481
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Entscheidung, wie ein Klick auf eine Quelle sie öffnet (E60). Reine Funktion, ohne Electron:
|
|
2
|
+
// der IPC-Handler in main.ts ruft sie und reicht das Ergebnis an `shell.openExternal` (nur für
|
|
3
|
+
// http/https) oder `shell.openPath` (lokaler Ordner) weiter — nichts anderes wird zugelassen,
|
|
4
|
+
// kein beliebiges Protokoll geht an eine der beiden Electron-Funktionen.
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { urlOrt } from "../kern/quelle-ort.js";
|
|
7
|
+
/**
|
|
8
|
+
* http(s)-Quellen öffnen ihre Adresse direkt im Browser. Eine SSH- oder scp-Adresse
|
|
9
|
+
* (`git@host:besitzer/repo.git`) hat keine Webseite als Adresse, aber Host, Besitzer und Repo sind
|
|
10
|
+
* bekannt (E36), also wird daraus die https-Repo-Seite gebaut — `shell.openExternal` bekommt so nie
|
|
11
|
+
* die ssh-Adresse selbst zu sehen. Lokale Quellen und `file://`-Quellen öffnen ihren Pfad im
|
|
12
|
+
* Finder. Alles, woraus sich weder eine http(s)-Adresse noch ein Pfad gewinnen lässt, wird
|
|
13
|
+
* abgelehnt: kein anderes Protokoll geht an eine Electron-Shell-Funktion.
|
|
14
|
+
*/
|
|
15
|
+
export function entscheideOeffnen(quelle) {
|
|
16
|
+
if (quelle.art === 'lokal')
|
|
17
|
+
return { art: 'ordner', pfad: quelle.pfad };
|
|
18
|
+
const text = quelle.url.trim();
|
|
19
|
+
if (/^https?:\/\//i.test(text))
|
|
20
|
+
return { art: 'browser', url: text };
|
|
21
|
+
if (/^file:\/\//i.test(text)) {
|
|
22
|
+
try {
|
|
23
|
+
return { art: 'ordner', pfad: fileURLToPath(text) };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { art: 'abgelehnt' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const ort = urlOrt(text);
|
|
30
|
+
if (ort && ort.host !== 'local')
|
|
31
|
+
return { art: 'browser', url: `https://${ort.host}/${ort.besitzer}/${ort.repo}` };
|
|
32
|
+
return { art: 'abgelehnt' };
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
4
|
+
contextBridge.exposeInMainWorld('hmgr', {
|
|
5
|
+
version: () => ipcRenderer.invoke('hmgr:version'),
|
|
6
|
+
/** Home-Verzeichnis, nur für pfadKurz im Renderer (Anzeige-Kürzung auf ~). */
|
|
7
|
+
home: () => ipcRenderer.invoke('hmgr:home'),
|
|
8
|
+
istZustand: () => ipcRenderer.invoke('hmgr:istZustand'),
|
|
9
|
+
marketplaces: () => ipcRenderer.invoke('hmgr:marketplaces'),
|
|
10
|
+
skills: () => ipcRenderer.invoke('hmgr:skills'),
|
|
11
|
+
quellen: () => ipcRenderer.invoke('hmgr:quellen'),
|
|
12
|
+
/** Icon einer Quelle als data-URL, oder null (E60). */
|
|
13
|
+
icon: (text) => ipcRenderer.invoke('hmgr:icon', text),
|
|
14
|
+
/** Öffnet eine Quelle: Repo-Seite im Browser oder Ordner im Finder (E60). */
|
|
15
|
+
quelleOeffnen: (text) => ipcRenderer.invoke('hmgr:quelleOeffnen', text),
|
|
16
|
+
quelleHinzufuegen: (text, name) => ipcRenderer.invoke('hmgr:quelleHinzufuegen', text, name),
|
|
17
|
+
quelleEntfernen: (text) => ipcRenderer.invoke('hmgr:quelleEntfernen', text),
|
|
18
|
+
/** Anzeigename einer Quelle ändern (E59), Identität bleibt ihre URL/ihr Pfad (E36). */
|
|
19
|
+
quelleUmbenennen: (text, name) => ipcRenderer.invoke('hmgr:quelleUmbenennen', text, name),
|
|
20
|
+
/** Aufräum-Angebot nach dem Entfernen einer Quelle (E58): Inventar vor quelleEntfernen erheben. */
|
|
21
|
+
quelleAufraeumInventar: (text) => ipcRenderer.invoke('hmgr:quelleAufraeumInventar', text),
|
|
22
|
+
quelleAufraeumen: (inventar) => ipcRenderer.invoke('hmgr:quelleAufraeumen', inventar),
|
|
23
|
+
linkLegen: (args) => ipcRenderer.invoke('hmgr:linkLegen', args),
|
|
24
|
+
linkEntfernen: (args) => ipcRenderer.invoke('hmgr:linkEntfernen', args),
|
|
25
|
+
eintragIgnorieren: (args) => ipcRenderer.invoke('hmgr:eintragIgnorieren', args),
|
|
26
|
+
eintragBeachten: (args) => ipcRenderer.invoke('hmgr:eintragBeachten', args),
|
|
27
|
+
eintragLoeschen: (args) => ipcRenderer.invoke('hmgr:eintragLoeschen', args),
|
|
28
|
+
uebernehmen: (args) => ipcRenderer.invoke('hmgr:uebernehmen', args),
|
|
29
|
+
aktualisieren: (args) => ipcRenderer.invoke('hmgr:aktualisieren', args),
|
|
30
|
+
arbeitsverzeichnisHinzufuegen: (pfad) => ipcRenderer.invoke('hmgr:arbeitsverzeichnisHinzufuegen', pfad),
|
|
31
|
+
arbeitsverzeichnisVergessen: (pfad) => ipcRenderer.invoke('hmgr:arbeitsverzeichnisVergessen', pfad),
|
|
32
|
+
/** Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). */
|
|
33
|
+
verzeichnisWaehlen: () => ipcRenderer.invoke('hmgr:verzeichnisWaehlen'),
|
|
34
|
+
harnessTypen: () => ipcRenderer.invoke('hmgr:harnessTypen'),
|
|
35
|
+
aktiveHarnesses: () => ipcRenderer.invoke('hmgr:aktiveHarnesses'),
|
|
36
|
+
nichtVorhandeneHarnesses: () => ipcRenderer.invoke('hmgr:nichtVorhandeneHarnesses'),
|
|
37
|
+
harnessAktivieren: (name) => ipcRenderer.invoke('hmgr:harnessAktivieren', name),
|
|
38
|
+
harnessDeaktivieren: (name) => ipcRenderer.invoke('hmgr:harnessDeaktivieren', name),
|
|
39
|
+
saeubern: (name) => ipcRenderer.invoke('hmgr:saeubern', name),
|
|
40
|
+
/** Verwalteter Hermes-Skill-Ordner (E56): zeigen kommt über hmgr:istZustand, hier nur Ändern. */
|
|
41
|
+
skillOrdnerFestlegen: (args) => ipcRenderer.invoke('hmgr:skillOrdnerFestlegen', args),
|
|
42
|
+
skillOrdnerEntfernen: (args) => ipcRenderer.invoke('hmgr:skillOrdnerEntfernen', args),
|
|
43
|
+
skillOrdnerLoeschen: (args) => ipcRenderer.invoke('hmgr:skillOrdnerLoeschen', args),
|
|
44
|
+
/** Rückfrage-Dialog (E28, E34): der Main-Prozess ruft, der Renderer meldet die Antwort zurück. */
|
|
45
|
+
onRueckfrage: (hoerer) => {
|
|
46
|
+
ipcRenderer.on('hmgr:rueckfrage', (_event, nachricht) => hoerer(nachricht));
|
|
47
|
+
},
|
|
48
|
+
antwortenRueckfrage: (id, antwort) => {
|
|
49
|
+
ipcRenderer.send('hmgr:rueckfrage:antwort', { id, antwort });
|
|
50
|
+
},
|
|
51
|
+
/** Anstoß zum selbsttätigen Aktualisieren (E44): Main ruft nach dem Start und alle sechs Stunden. */
|
|
52
|
+
onAktualisierenAnstossen: (hoerer) => {
|
|
53
|
+
ipcRenderer.on('hmgr:aktualisierenAnstossen', () => hoerer());
|
|
54
|
+
},
|
|
55
|
+
});
|