hmgr 0.0.4 → 0.0.6
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/README.md +10 -0
- package/dist/adapter/checkout-pfad.js +10 -2
- package/dist/adapter/claude-code.js +251 -23
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +146 -8
- package/dist/adapter/git.js +129 -17
- package/dist/adapter/hermes.js +235 -10
- package/dist/adapter/marketplace-json.js +2 -1
- package/dist/adapter/mcp-katalog.js +124 -0
- package/dist/adapter/quelle-text.js +10 -1
- package/dist/adapter/skill-md.js +96 -19
- package/dist/adapter/terminal.js +1 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +6 -1
- package/dist/cli/hilfe.js +227 -26
- package/dist/cli/main.js +362 -20
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/icons.js +114 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +823 -0
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +33 -0
- package/dist/gui/preload.cjs +70 -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 +581 -0
- package/dist/gui/renderer/renderer.js +3227 -0
- package/dist/gui/renderer/styles.css +701 -0
- package/dist/kern/anwendungsfaelle.js +859 -65
- package/dist/kern/modell.js +297 -0
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +16 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +47 -2
package/dist/gui/main.js
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
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, Menu, shell } from 'electron';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { stat } from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
11
|
+
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
12
|
+
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
13
|
+
import { kannProfile } from "../ports/harness.js";
|
|
14
|
+
import { iconFuerQuelle } from "./icons.js";
|
|
15
|
+
import { entscheideOeffnen } from "./oeffnen.js";
|
|
16
|
+
import { mcpUebernahmeSerialisieren, mcpUebersichtSerialisieren, mcpWirkungSerialisieren } from "./mcp-ansicht.js";
|
|
17
|
+
import { beobachten } from "./beobachter.js";
|
|
18
|
+
import { loginPathUebernehmen } from "./login-pfad.js";
|
|
19
|
+
const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
|
|
21
|
+
/**
|
|
22
|
+
* Nur eine hmgr-Instanz je Nutzer (E67). Grund: zwei laufende Fenster schrieben nacheinander auf
|
|
23
|
+
* dieselbe `state.json` und überschrieben sich gegenseitig — der Zustand ist einer, nicht einer je
|
|
24
|
+
* Fenster. Die zweite Instanz bekommt den Lock nicht und beendet sich; die erste holt in
|
|
25
|
+
* `second-instance` ihr Fenster nach vorn, damit der zweite Start nicht wirkungslos aussieht.
|
|
26
|
+
* Gilt auf allen Plattformen: unter macOS ist der Doppelstart selten (das Dock startet nicht neu),
|
|
27
|
+
* unter Windows ist ein zweiter Klick im Startmenü der Normalfall.
|
|
28
|
+
*
|
|
29
|
+
* Der Lock wird ganz früh geholt, noch vor `bauen()` und der PATH-Ermittlung: eine Instanz, die
|
|
30
|
+
* ohnehin gleich wieder geht, soll nichts mehr anfassen und keine Shell mehr starten.
|
|
31
|
+
*
|
|
32
|
+
* Im Smoke-Test ist er aus. Er hängt am Electron-userData-Verzeichnis, und das leitet macOS nicht
|
|
33
|
+
* über das gefälschte HOME um (gemessen, siehe E67) — eine offene hmgr-GUI auf dem Rechner würde
|
|
34
|
+
* den Smoke-Lauf sonst stumm abwürgen. Dieselbe Zurückhaltung wie bei Watcher und
|
|
35
|
+
* Auto-Aktualisieren: was einen automatisierten Lauf von außen stören kann, bleibt im Smoke aus.
|
|
36
|
+
*/
|
|
37
|
+
const einzelinstanz = smoketest || app.requestSingleInstanceLock();
|
|
38
|
+
if (!einzelinstanz)
|
|
39
|
+
app.quit();
|
|
40
|
+
const umgebung = umgebungLesen();
|
|
41
|
+
/** Cache der Quellen-Icons (E60), unterhalb von HMGR_HOME wie der Zustand: Tests/Smoke fassen so
|
|
42
|
+
nie ein echtes Verzeichnis an. */
|
|
43
|
+
const iconCacheVerzeichnis = path.join(umgebung.hmgrHome, 'icon-cache');
|
|
44
|
+
/** Intervall des selbsttätigen Aktualisierens (E44): fest, keine Einstellung. */
|
|
45
|
+
const SECHS_STUNDEN_MS = 6 * 60 * 60 * 1000;
|
|
46
|
+
/**
|
|
47
|
+
* Fake der claude-CLI für den Smoke-Test (E40 R4), nach dem Muster von `fakeClaude` in
|
|
48
|
+
* tests/helfer.ts. Kein Kindprozess, keine echte CLI: ein automatisierter Lauf fasst die echte
|
|
49
|
+
* claude-CLI formal nie an, auch wenn sie auf dem PATH liegt.
|
|
50
|
+
*
|
|
51
|
+
* `--version` läuft durch, damit Claude Code im Smoke-Test seine Fähigkeiten bekommt (E61: die
|
|
52
|
+
* MCP-Fähigkeit hängt an der vorhandenen CLI). Sonst blieben die Claude-Code-Zeilen der MCP-Ansicht
|
|
53
|
+
* ungetestet. Jeder andere Aufruf scheitert mit Code 1: die lesenden Pfade tragen das (Plugin-Liste
|
|
54
|
+
* leer, `eingehaengte` liest ohnehin nur ~/.claude.json), und die schreibenden bleiben so
|
|
55
|
+
* verschlossen — der Smoke-Test hängt nichts ein und aus, er öffnet die Dialoge nur.
|
|
56
|
+
*/
|
|
57
|
+
const smoketestClaudeAusfuehrer = async (args) => args[0] === '--version' ? { stdout: 'claude 0.0.0-smoke\n', code: 0 } : { stdout: '', code: 1 };
|
|
58
|
+
let hauptfenster = null;
|
|
59
|
+
/**
|
|
60
|
+
* Rückfrage-Port als Dialog im Renderer (E28, E34): der Main-Prozess schickt die Frage per
|
|
61
|
+
* `webContents.send`, der Renderer zeigt einen <dialog> im Stil des Entwurfs und schickt die
|
|
62
|
+
* Antwort über einen Kanal mit Korrelations-ID zurück. Ohne offenes Fenster kann niemand antworten,
|
|
63
|
+
* dort gilt wie am Terminal ohne TTY: Nein bzw. keine Antwort (E34).
|
|
64
|
+
*/
|
|
65
|
+
class GuiRueckfrage {
|
|
66
|
+
ausstehend = new Map();
|
|
67
|
+
constructor() {
|
|
68
|
+
ipcMain.on('hmgr:rueckfrage:antwort', (_event, nachricht) => {
|
|
69
|
+
const aufloesen = this.ausstehend.get(nachricht.id);
|
|
70
|
+
if (aufloesen === undefined)
|
|
71
|
+
return;
|
|
72
|
+
this.ausstehend.delete(nachricht.id);
|
|
73
|
+
aufloesen(nachricht.antwort);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
stellen(payload) {
|
|
77
|
+
if (hauptfenster === null)
|
|
78
|
+
return Promise.resolve(payload.art === 'bestaetigen' ? false : undefined);
|
|
79
|
+
const id = randomUUID();
|
|
80
|
+
return new Promise((aufloesen) => {
|
|
81
|
+
this.ausstehend.set(id, aufloesen);
|
|
82
|
+
hauptfenster?.webContents.send('hmgr:rueckfrage', { id, ...payload });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async bestaetigen(frage) {
|
|
86
|
+
const antwort = await this.stellen({ art: 'bestaetigen', frage });
|
|
87
|
+
return antwort === true;
|
|
88
|
+
}
|
|
89
|
+
async auswaehlen(frage, optionen) {
|
|
90
|
+
const antwort = await this.stellen({ art: 'auswaehlen', frage, optionen });
|
|
91
|
+
return typeof antwort === 'number' ? antwort : undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* PATH aus der Login-Shell (F47=O2, E63): aus dem Finder gestartet bekommt die App nur den
|
|
96
|
+
* macOS-Minimal-PATH, ohne z. B. ~/.local/bin mit der claude-CLI. Die Ermittlung läuft ab hier
|
|
97
|
+
* parallel zum Electron-Start; gewartet wird zentral in `auffangen`, und damit vor jedem
|
|
98
|
+
* Adapter-Aufruf, denn jeder IPC-Handler geht durch `auffangen` und Spawns gibt es nur dort
|
|
99
|
+
* (claude-code.ts, git.ts — Watcher, Fokus-Reload und Auto-Aktualisieren stoßen den Renderer an,
|
|
100
|
+
* der wieder über IPC hereinkommt).
|
|
101
|
+
*
|
|
102
|
+
* Bewusst kein Top-Level-`await`: im ESM-Main-Entry läuft vor `ready` nur, was die Imports als
|
|
103
|
+
* Nebeneffekt tun; ein Await dort verschiebt die Registrierung aller App-Handler und verzögert
|
|
104
|
+
* das Fenster um die Laufzeit der Shell (~2 s). So erscheint das Fenster sofort, und höchstens
|
|
105
|
+
* der erste Datenabruf wartet. Im Smoke-Test unterbleibt die Ermittlung ganz (nie eine echte
|
|
106
|
+
* Shell, Muster: Watcher/Auto-Aktualisieren sind im Smoke auch aus). Der `catch` ist die letzte
|
|
107
|
+
* Leine: dieser Aufruf darf die App unter keinen Umständen stören.
|
|
108
|
+
*
|
|
109
|
+
* Unter Windows steigt `loginPathUebernehmen` selbst sofort aus (E67): dort gibt es keine
|
|
110
|
+
* Login-Shell im POSIX-Sinn und keine PATH-Lücke zu füllen.
|
|
111
|
+
*/
|
|
112
|
+
const pathBereit = smoketest || !einzelinstanz
|
|
113
|
+
? Promise.resolve()
|
|
114
|
+
: loginPathUebernehmen().catch(() => undefined);
|
|
115
|
+
const rueckfrage = new GuiRueckfrage();
|
|
116
|
+
const { anwendungsfaelle, ports } = bauen(umgebung, {
|
|
117
|
+
rueckfrage,
|
|
118
|
+
...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
|
|
119
|
+
});
|
|
120
|
+
async function auffangen(fn) {
|
|
121
|
+
await pathBereit; // erst der Login-PATH (F47=O2), dann der Adapter — sonst findet er die CLI nicht
|
|
122
|
+
try {
|
|
123
|
+
return { ok: true, daten: await fn() };
|
|
124
|
+
}
|
|
125
|
+
catch (fehler) {
|
|
126
|
+
return { ok: false, fehler: fehler instanceof Error ? fehler.message : String(fehler) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** ZielZustand über IPC: Maps werden zu einfachen Objekten, der Rest ist schon JSON-tauglich. */
|
|
130
|
+
function zielZustandSerialisieren(z) {
|
|
131
|
+
return {
|
|
132
|
+
ziel: z.ziel,
|
|
133
|
+
verzeichnis: z.verzeichnis,
|
|
134
|
+
eintraege: z.eintraege,
|
|
135
|
+
einschraenkungen: Object.fromEntries(z.einschraenkungen),
|
|
136
|
+
gruende: Object.fromEntries(z.gruende),
|
|
137
|
+
ignoriert: z.ignoriert,
|
|
138
|
+
uebernahme: Object.fromEntries(z.uebernahme),
|
|
139
|
+
istRegistriert: z.istRegistriert,
|
|
140
|
+
kannSkillOrdner: z.kannSkillOrdner,
|
|
141
|
+
...(z.plugins === undefined ? {} : { plugins: z.plugins }),
|
|
142
|
+
...(z.verwalteterSkillOrdner === undefined ? {} : { verwalteterSkillOrdner: z.verwalteterSkillOrdner }),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function zielAusIpc(harness, ort) {
|
|
146
|
+
return { harness, ort };
|
|
147
|
+
}
|
|
148
|
+
function registriereIpc() {
|
|
149
|
+
ipcMain.handle('hmgr:version', async () => auffangen(async () => app.getVersion()));
|
|
150
|
+
// Für pfadKurz im Renderer (Anzeige-Kürzung auf ~): nur lesend, nie in einen anderen Aufruf
|
|
151
|
+
// zurückgespeist. Dasselbe Home wie der Kern (`umgebungLesen`: `HOME` vor `os.homedir()`, E68) —
|
|
152
|
+
// sonst kürzt die Anzeige dort nicht, wo HOME abweichend gesetzt ist (MSYS/Git-Bash).
|
|
153
|
+
ipcMain.handle('hmgr:home', async () => auffangen(async () => umgebung.home));
|
|
154
|
+
// Ebenfalls nur für die Anzeige (E68): der Renderer leitet daraus den Pfadtrenner ab, mit dem er
|
|
155
|
+
// eigene Anzeigepfade zusammensetzt („~/.claude.json" bzw. „~\.claude.json").
|
|
156
|
+
ipcMain.handle('hmgr:plattform', async () => auffangen(async () => process.platform));
|
|
157
|
+
ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand()).map(zielZustandSerialisieren)));
|
|
158
|
+
ipcMain.handle('hmgr:marketplaces', async () => auffangen(async () => (await anwendungsfaelle.marketplaces()).map((m) => ({
|
|
159
|
+
quelleText: quelleAlsText(m.quelle),
|
|
160
|
+
name: m.name,
|
|
161
|
+
plugins: m.plugins,
|
|
162
|
+
}))));
|
|
163
|
+
// Skill-Katalog über alle bekannten Quellen (E40 R3): über den Anwendungsfall `katalog`, nicht
|
|
164
|
+
// mehr direkt über die Ports. Eine nicht erreichbare Quelle trägt einen Fehlervermerk (E12-Muster
|
|
165
|
+
// wie istZustand/marketplaces), ihre Skills fehlen einfach in der flachen Liste.
|
|
166
|
+
ipcMain.handle('hmgr:skills', async () => auffangen(async () => {
|
|
167
|
+
const ergebnis = [];
|
|
168
|
+
for (const eintrag of await anwendungsfaelle.katalog()) {
|
|
169
|
+
if (eintrag.ref === undefined)
|
|
170
|
+
continue; // nicht erreichbar, keine Skills
|
|
171
|
+
for (const skill of eintrag.skills)
|
|
172
|
+
ergebnis.push({
|
|
173
|
+
name: skill.name,
|
|
174
|
+
pfad: skill.pfad,
|
|
175
|
+
...(skill.beschreibung === undefined ? {} : { beschreibung: skill.beschreibung }),
|
|
176
|
+
...(skill.nurPerBefehl === undefined ? {} : { nurPerBefehl: skill.nurPerBefehl }),
|
|
177
|
+
...(skill.nichtPerBefehl === undefined ? {} : { nichtPerBefehl: skill.nichtPerBefehl }),
|
|
178
|
+
...(skill.registriertHooks === undefined ? {} : { registriertHooks: skill.registriertHooks }),
|
|
179
|
+
...(skill.werkzeugeErlaubt === undefined ? {} : { werkzeugeErlaubt: skill.werkzeugeErlaubt }),
|
|
180
|
+
...(skill.kompatibilitaet === undefined ? {} : { kompatibilitaet: skill.kompatibilitaet }),
|
|
181
|
+
...(skill.modell === undefined ? {} : { modell: skill.modell }),
|
|
182
|
+
...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
|
|
183
|
+
...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
|
|
184
|
+
quelleText: quelleAlsText(eintrag.quelle),
|
|
185
|
+
quelleName: quelleName(eintrag.quelle),
|
|
186
|
+
quelleSchluessel: quelleSchluessel(eintrag.quelle),
|
|
187
|
+
ref: eintrag.ref,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return ergebnis;
|
|
191
|
+
}));
|
|
192
|
+
// Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
|
|
193
|
+
ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
|
|
194
|
+
art: quelle.art,
|
|
195
|
+
text: quelleAlsText(quelle),
|
|
196
|
+
name: quelleName(quelle),
|
|
197
|
+
}))));
|
|
198
|
+
// Quelle-Verwaltung in den Einstellungen (E54, E59): Text-zu-Quelle passiert im Main-Prozess, der
|
|
199
|
+
// Renderer kennt nur Zeichenketten. Der Anzeigename kommt serverseitig mit, damit dem Renderer nie
|
|
200
|
+
// ein weggefiltertes Feld fehlt (E59: „name" ist reine Anzeige, geht nicht in quelleAusText ein).
|
|
201
|
+
ipcMain.handle('hmgr:quelleHinzufuegen', async (_event, text, name) => auffangen(async () => {
|
|
202
|
+
const ergebnis = await anwendungsfaelle.quelleHinzufuegen(quelleAusText(text), name);
|
|
203
|
+
return {
|
|
204
|
+
quelle: {
|
|
205
|
+
art: ergebnis.quelle.art,
|
|
206
|
+
text: quelleAlsText(ergebnis.quelle),
|
|
207
|
+
name: quelleName(ergebnis.quelle),
|
|
208
|
+
},
|
|
209
|
+
skills: ergebnis.skills,
|
|
210
|
+
marketplace: ergebnis.marketplace,
|
|
211
|
+
bekannt: ergebnis.bekannt,
|
|
212
|
+
};
|
|
213
|
+
}));
|
|
214
|
+
ipcMain.handle('hmgr:quelleEntfernen', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleEntfernen(quelleAusText(text))));
|
|
215
|
+
ipcMain.handle('hmgr:quelleUmbenennen', async (_event, text, name) => auffangen(async () => {
|
|
216
|
+
const umbenannt = await anwendungsfaelle.quelleUmbenennen(quelleAusText(text), name);
|
|
217
|
+
return { art: umbenannt.art, text: quelleAlsText(umbenannt), name: quelleName(umbenannt) };
|
|
218
|
+
}));
|
|
219
|
+
// Aufräum-Angebot nach dem Entfernen einer Quelle (E58): Inventar vor dem eigentlichen Entfernen
|
|
220
|
+
// erheben (der Renderer ruft das, bevor er quelleEntfernen aufruft), dann bei Zustimmung abräumen.
|
|
221
|
+
// Beide Ergebnisse reichen unverändert durch: schon reine Daten, keine Maps oder Funktionen darin.
|
|
222
|
+
ipcMain.handle('hmgr:quelleAufraeumInventar', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleAufraeumInventar(quelleAusText(text))));
|
|
223
|
+
ipcMain.handle('hmgr:quelleAufraeumen', async (_event, inventar) => auffangen(async () => anwendungsfaelle.quelleAufraeumen(inventar)));
|
|
224
|
+
// Die GUI arbeitet an einem Arbeitsverzeichnis wie die CLI mit --local (E57, F32): Links werden
|
|
225
|
+
// lokal gelegt und entfernt, der Zeiger bleibt unangetastet, der Kern hängt bei einer bestehenden
|
|
226
|
+
// Bindung von sich aus einen Hinweis an. Für Home und Profil ist `lokal` folgenlos (E26).
|
|
227
|
+
ipcMain.handle('hmgr:linkLegen', async (_event, args) => auffangen(async () => anwendungsfaelle.linkLegen({
|
|
228
|
+
skill: args.skill,
|
|
229
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
230
|
+
...(args.quelle === undefined ? {} : { quelle: args.quelle }),
|
|
231
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
232
|
+
})));
|
|
233
|
+
ipcMain.handle('hmgr:linkEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.linkEntfernen({
|
|
234
|
+
skill: args.skill,
|
|
235
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
236
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
237
|
+
})));
|
|
238
|
+
ipcMain.handle('hmgr:eintragIgnorieren', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragIgnorieren({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
239
|
+
ipcMain.handle('hmgr:eintragBeachten', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragBeachten({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
240
|
+
// Auch die Übernahme legt Links und schriebe ohne `lokal` den Zeiger (E31 über linkAusFund), sie
|
|
241
|
+
// trifft also dieselbe Sperre wie link (E57). In der GUI arbeitet sie darum wie die CLI mit
|
|
242
|
+
// --local (F32), sonst bräche der Knopf an jedem gebundenen Arbeitsverzeichnis ab.
|
|
243
|
+
ipcMain.handle('hmgr:uebernehmen', async (_event, args) => auffangen(async () => anwendungsfaelle.uebernehmen({
|
|
244
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
245
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
246
|
+
probelauf: args.probelauf,
|
|
247
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
248
|
+
})));
|
|
249
|
+
ipcMain.handle('hmgr:eintragLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragLoeschen({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
250
|
+
ipcMain.handle('hmgr:aktualisieren', async (_event, args) => auffangen(async () => anwendungsfaelle.aktualisieren(args)));
|
|
251
|
+
ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
|
|
252
|
+
ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
|
|
253
|
+
// Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). Im Smoke-Test darf
|
|
254
|
+
// nie ein echter Systemdialog aufgehen: sofort null, wie bei Abbruch.
|
|
255
|
+
ipcMain.handle('hmgr:verzeichnisWaehlen', async () => auffangen(async () => {
|
|
256
|
+
if (smoketest || hauptfenster === null)
|
|
257
|
+
return null;
|
|
258
|
+
const ergebnis = await dialog.showOpenDialog(hauptfenster, {
|
|
259
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
260
|
+
});
|
|
261
|
+
if (ergebnis.canceled || ergebnis.filePaths.length === 0)
|
|
262
|
+
return null;
|
|
263
|
+
return ergebnis.filePaths[0] ?? null;
|
|
264
|
+
}));
|
|
265
|
+
// Harness-Schalter im Einstellungen-Modal (E38, GRILL.md Entwurfskritik 4).
|
|
266
|
+
ipcMain.handle('hmgr:harnessTypen', async () => auffangen(async () => anwendungsfaelle.harnessTypen()));
|
|
267
|
+
ipcMain.handle('hmgr:aktiveHarnesses', async () => auffangen(async () => anwendungsfaelle.aktiveHarnesses()));
|
|
268
|
+
// Für den Vermerk „nicht installiert" an den Schaltern (E42).
|
|
269
|
+
ipcMain.handle('hmgr:nichtVorhandeneHarnesses', async () => auffangen(async () => anwendungsfaelle.nichtVorhandeneHarnesses()));
|
|
270
|
+
ipcMain.handle('hmgr:harnessAktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessAktivieren(name)));
|
|
271
|
+
ipcMain.handle('hmgr:harnessDeaktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessDeaktivieren(name)));
|
|
272
|
+
ipcMain.handle('hmgr:saeubern', async (_event, name) => auffangen(async () => anwendungsfaelle.saeubern(name)));
|
|
273
|
+
// Verwalteter Hermes-Skill-Ordner (E56): Zeigen kommt schon über hmgr:istZustand, hier nur Ändern.
|
|
274
|
+
ipcMain.handle('hmgr:skillOrdnerFestlegen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerFestlegen(zielAusIpc(args.harness, args.ort), args.pfad)));
|
|
275
|
+
ipcMain.handle('hmgr:skillOrdnerEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerEntfernen(zielAusIpc(args.harness, args.ort))));
|
|
276
|
+
ipcMain.handle('hmgr:skillOrdnerLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerLoeschen(zielAusIpc(args.harness, args.ort), args.pfad)));
|
|
277
|
+
// Quellen-Icon (E60): nur im Main-Prozess geladen, als data-URL geliefert (CSP erlaubt img-src
|
|
278
|
+
// data:). Im Smoke-Test nie ein Netzzugriff: sofort null, der Renderer zeigt dann das generische
|
|
279
|
+
// Symbol. Ein Fehlschlag wirft nie (auffangen fängt ihn, iconFuerQuelle selbst wirft ohnehin nicht).
|
|
280
|
+
ipcMain.handle('hmgr:icon', async (_event, quelleText) => auffangen(async () => {
|
|
281
|
+
if (smoketest)
|
|
282
|
+
return null;
|
|
283
|
+
return iconFuerQuelle(quelleAusText(quelleText), { cacheVerzeichnis: iconCacheVerzeichnis });
|
|
284
|
+
}));
|
|
285
|
+
// Quelle öffnen (E60): die Protokoll-Weiche steht in oeffnen.ts (rein, testbar), hier nur die
|
|
286
|
+
// Wirkung. `shell.openExternal` bekommt ausschließlich http(s)-URLs, `shell.openPath` nur einen
|
|
287
|
+
// Ordner, den es wirklich gibt — nichts anderes wird zugelassen. Alles, was es gibt, aber kein
|
|
288
|
+
// Ordner ist, wird im Dateimanager nur gezeigt, nicht geöffnet: `openPath` startet sonst eine Datei
|
|
289
|
+
// mit ihrem Standardprogramm, und ein Pfad aus dem Renderer soll nichts starten können.
|
|
290
|
+
ipcMain.handle('hmgr:quelleOeffnen', async (_event, quelleText) => auffangen(async () => {
|
|
291
|
+
const entscheidung = entscheideOeffnen(quelleAusText(quelleText));
|
|
292
|
+
if (entscheidung.art === 'browser')
|
|
293
|
+
await shell.openExternal(entscheidung.url);
|
|
294
|
+
else if (entscheidung.art === 'ordner') {
|
|
295
|
+
const info = await stat(entscheidung.pfad).catch(() => undefined);
|
|
296
|
+
if (info === undefined)
|
|
297
|
+
throw new Error(`Es gibt „${entscheidung.pfad}" nicht (mehr).`);
|
|
298
|
+
if (info.isDirectory())
|
|
299
|
+
await shell.openPath(entscheidung.pfad);
|
|
300
|
+
else
|
|
301
|
+
shell.showItemInFolder(entscheidung.pfad);
|
|
302
|
+
}
|
|
303
|
+
return entscheidung;
|
|
304
|
+
}));
|
|
305
|
+
// MCP-Ansicht (E61-Folgerunde): Whitelisting und der F39-Vermerk je Karte stehen in mcp-ansicht.ts
|
|
306
|
+
// (rein, ohne Electron, wie oeffnen.ts und icons.ts), damit sie testbar sind. Der Main-Prozess
|
|
307
|
+
// ruft nur noch auf und liefert die eigene Prozess-Umgebung für die F39-Prüfung.
|
|
308
|
+
ipcMain.handle('hmgr:mcpUebersicht', async () => auffangen(async () => mcpUebersichtSerialisieren(await anwendungsfaelle.mcpUebersicht(), process.env)));
|
|
309
|
+
// Plätze für den Einhängen-Dialog (F42): die Fähigkeit hängt am Adapter, nicht am Namen (E16),
|
|
310
|
+
// deshalb kommt die Liste unverändert aus dem Kern statt aus einer Harness-Fallunterscheidung hier.
|
|
311
|
+
ipcMain.handle('hmgr:mcpPlaetze', async () => auffangen(async () => anwendungsfaelle.mcpPlaetze()));
|
|
312
|
+
ipcMain.handle('hmgr:mcpEinhaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpEinhaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
313
|
+
ipcMain.handle('hmgr:mcpAushaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpAushaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
314
|
+
// Übernehmen aus der geteilten .mcp.json (E64): die Bestätigung holt der Renderer ein, hier läuft
|
|
315
|
+
// nur die Ausführung. Der Name muss nicht im Katalog stehen.
|
|
316
|
+
ipcMain.handle('hmgr:mcpUebernehmen', async (_event, args) => auffangen(async () => mcpUebernahmeSerialisieren(await anwendungsfaelle.mcpUebernehmen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
317
|
+
}
|
|
318
|
+
/** Mindestabstand zwischen zwei Fokus-Reloads (E62): schnelles Fenster-Wechseln löst keine Lawine
|
|
319
|
+
von Anzeige-Neuladungen aus. Getrennt vom Entprellen im Datei-Watcher (das sammelt Events vor dem
|
|
320
|
+
ersten Aufruf), hier wird zusätzlich zwischen aufeinanderfolgenden Aufrufen gedrosselt (trailing:
|
|
321
|
+
ein Aufruf innerhalb der Sperrzeit wird nicht verworfen, sondern ans Ende der Sperrzeit verschoben). */
|
|
322
|
+
const FOKUS_RELOAD_MIN_ABSTAND_MS = 3000;
|
|
323
|
+
/** Sammelzeit des Datei-Watchers (E62): mehrere Events in kurzer Folge lösen einen Reload aus. */
|
|
324
|
+
const BEOBACHTER_ENTPRELLEN_MS = 400;
|
|
325
|
+
/**
|
|
326
|
+
* Pfade, die die GUI kennt und darum beobachtet (E62): hmgr-Zustand samt MCP-Katalog, die
|
|
327
|
+
* Harness-MCP-Konfigurationen (`~/.claude.json`, Hermes' `config.yaml`) und die `.env`-Dateien, die
|
|
328
|
+
* die F39-Prüfung beim MCP-Einhängen liest — bei Hermes liegt die `.env` je Ort neben der
|
|
329
|
+
* `config.yaml` (so liest sie der Adapter in `mcp.envDatei`), darum auch je Profil. Die Profile
|
|
330
|
+
* werden einmal beim Start am Harness selbst erfragt (`profile()`, ein Verzeichnis-Blick, kein
|
|
331
|
+
* ganzer Erhebungslauf); ein zur Laufzeit neu angelegtes Profil beobachtet der Watcher nicht
|
|
332
|
+
* dynamisch, das deckt der Fokus-Reload ab (E62).
|
|
333
|
+
*
|
|
334
|
+
* Nicht dabei: die geteilten `.mcp.json` der Arbeitsverzeichnisse, die die MCP-Übersicht seit dem
|
|
335
|
+
* E61-Nachtrag mitliest. Ihre Liste ist zur Laufzeit veränderlich, wie die der Hermes-Profile, und
|
|
336
|
+
* damit gilt für sie dasselbe: der Fokus-Reload deckt sie ab.
|
|
337
|
+
*/
|
|
338
|
+
async function beobachtetePfade() {
|
|
339
|
+
const hermesHome = path.join(umgebung.home, '.hermes');
|
|
340
|
+
const pfade = [
|
|
341
|
+
path.join(umgebung.hmgrHome, 'state.json'),
|
|
342
|
+
path.join(umgebung.hmgrHome, 'mcp.yaml'),
|
|
343
|
+
path.join(umgebung.home, '.claude.json'),
|
|
344
|
+
path.join(hermesHome, 'config.yaml'),
|
|
345
|
+
path.join(hermesHome, '.env'),
|
|
346
|
+
];
|
|
347
|
+
const hermes = ports.harnesses.find((h) => h.name === 'hermes');
|
|
348
|
+
if (hermes !== undefined && kannProfile(hermes))
|
|
349
|
+
for (const name of await hermes.profile().catch(() => [])) {
|
|
350
|
+
const profilHome = path.join(hermesHome, 'profiles', name);
|
|
351
|
+
pfade.push(path.join(profilHome, 'config.yaml'), path.join(profilHome, '.env'));
|
|
352
|
+
}
|
|
353
|
+
return pfade;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Stößt beim Renderer ein stilles Neuladen der Anzeige an (E62), nie einen Aktualisieren-Lauf mit
|
|
357
|
+
* Netzzugriff. Gedrosselt (trailing throttle): unmittelbar, wenn seit dem letzten Anstoß genug Zeit
|
|
358
|
+
* verging, sonst genau ein nachgeholter Anstoß am Ende der Sperrzeit.
|
|
359
|
+
*/
|
|
360
|
+
function neuLadenAnstossenFabrik(minAbstandMs) {
|
|
361
|
+
let letzter = 0;
|
|
362
|
+
let timer;
|
|
363
|
+
return function anstossen() {
|
|
364
|
+
const jetzt = Date.now();
|
|
365
|
+
const seitLetztem = jetzt - letzter;
|
|
366
|
+
if (seitLetztem >= minAbstandMs) {
|
|
367
|
+
letzter = jetzt;
|
|
368
|
+
hauptfenster?.webContents.send('hmgr:neuLadenAnstossen');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (timer !== undefined)
|
|
372
|
+
return; // schon ein Anstoß fürs Ende der Sperrzeit vorgemerkt
|
|
373
|
+
timer = setTimeout(() => {
|
|
374
|
+
timer = undefined;
|
|
375
|
+
letzter = Date.now();
|
|
376
|
+
hauptfenster?.webContents.send('hmgr:neuLadenAnstossen');
|
|
377
|
+
}, minAbstandMs - seitLetztem);
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const neuLadenAnstossen = neuLadenAnstossenFabrik(FOKUS_RELOAD_MIN_ABSTAND_MS);
|
|
381
|
+
let beobachter;
|
|
382
|
+
function fensterOeffnen() {
|
|
383
|
+
hauptfenster = new BrowserWindow({
|
|
384
|
+
width: 1280,
|
|
385
|
+
height: 860,
|
|
386
|
+
minWidth: 860,
|
|
387
|
+
minHeight: 560,
|
|
388
|
+
backgroundColor: '#E3E7E0', // --platte, verhindert einen weißen Blitz beim Start
|
|
389
|
+
// Im Smoke-Test bleibt das Fenster unsichtbar: der Renderer lädt trotzdem vollständig, der
|
|
390
|
+
// DOM-Auszug kommt per executeJavaScript. So klaut ein automatisierter Lauf niemandem den
|
|
391
|
+
// Eingabefokus und öffnet kein sichtbares Fenster.
|
|
392
|
+
show: !smoketest,
|
|
393
|
+
webPreferences: {
|
|
394
|
+
preload: path.join(hierVerzeichnis, 'preload.cjs'),
|
|
395
|
+
contextIsolation: true,
|
|
396
|
+
nodeIntegration: false,
|
|
397
|
+
sandbox: true,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
hauptfenster.on('closed', () => {
|
|
401
|
+
hauptfenster = null;
|
|
402
|
+
});
|
|
403
|
+
// Renderer-Konsole im Main-Log, für Fehlersuche und den Smoke-Test.
|
|
404
|
+
hauptfenster.webContents.on('console-message', (event) => {
|
|
405
|
+
console.log(`[renderer] ${event.message}`);
|
|
406
|
+
});
|
|
407
|
+
// Nur für den Smoke-Test (scripts/gui-smoke-test.ts): ein DOM-Auszug im Log, danach Ende.
|
|
408
|
+
if (smoketest) {
|
|
409
|
+
const fenster = hauptfenster;
|
|
410
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
411
|
+
setTimeout(() => {
|
|
412
|
+
void fenster.webContents
|
|
413
|
+
.executeJavaScript(`(async () => {
|
|
414
|
+
// Einstellungen-Modal (E40 R2): öffnen, die Harness-Schalter ablesen, wieder schließen.
|
|
415
|
+
document.getElementById('einstellungen')?.click();
|
|
416
|
+
var einstellungenOffen = document.getElementById('d-einstellungen')?.open === true;
|
|
417
|
+
var harnessSchalter = Array.from(document.querySelectorAll('#harnessliste input[type=checkbox]'))
|
|
418
|
+
.map((i) => i.dataset.harness + ':' + i.checked)
|
|
419
|
+
.join(',');
|
|
420
|
+
// Quellen-Abschnitt der Einstellungen (E54): Knopf und Dialoge vorhanden, der
|
|
421
|
+
// Hinzufügen-Dialog lässt sich öffnen und per Abbrechen ohne Fehler wieder schließen.
|
|
422
|
+
// Kein echter Quellen-Lauf: der Abbrechen-Knopf löst keinen IPC-Aufruf aus.
|
|
423
|
+
var quelleneuVorhanden = document.getElementById('quelleneu') !== null;
|
|
424
|
+
var dialogeQuelleVorhanden =
|
|
425
|
+
document.getElementById('d-quelle') !== null && document.getElementById('d-quelle-vergessen') !== null;
|
|
426
|
+
var quelleFehler = null;
|
|
427
|
+
var dQuelleOffenNachOeffnen = false;
|
|
428
|
+
var dQuelleOffenNachAbbrechen = true;
|
|
429
|
+
try {
|
|
430
|
+
document.getElementById('quelleneu')?.click();
|
|
431
|
+
dQuelleOffenNachOeffnen = document.getElementById('d-quelle')?.open === true;
|
|
432
|
+
document.querySelector('#d-quelle button[value="abbruch"]')?.click();
|
|
433
|
+
dQuelleOffenNachAbbrechen = document.getElementById('d-quelle')?.open === true;
|
|
434
|
+
} catch (fehler) {
|
|
435
|
+
quelleFehler = String(fehler);
|
|
436
|
+
}
|
|
437
|
+
// Aufräum-Angebot nach Quelle entfernen (E58): kein echter Lauf (der würde Checkouts
|
|
438
|
+
// und Links wirklich löschen), nur ob der Dialog existiert und sich öffnen und über
|
|
439
|
+
// Behalten schließen lässt, ohne Fehler.
|
|
440
|
+
var dialogQuelleAufraeumenVorhanden = document.getElementById('d-quelle-aufraeumen') !== null;
|
|
441
|
+
var quelleAufraeumenFehler = null;
|
|
442
|
+
var dQuelleAufraeumenOffenNachOeffnen = false;
|
|
443
|
+
var dQuelleAufraeumenOffenNachBehalten = true;
|
|
444
|
+
try {
|
|
445
|
+
var dQuelleAufraeumen = document.getElementById('d-quelle-aufraeumen');
|
|
446
|
+
dQuelleAufraeumen?.showModal();
|
|
447
|
+
dQuelleAufraeumenOffenNachOeffnen = dQuelleAufraeumen?.open === true;
|
|
448
|
+
document.querySelector('#d-quelle-aufraeumen button[value="behalten"]')?.click();
|
|
449
|
+
dQuelleAufraeumenOffenNachBehalten = dQuelleAufraeumen?.open === true;
|
|
450
|
+
} catch (fehler) {
|
|
451
|
+
quelleAufraeumenFehler = String(fehler);
|
|
452
|
+
}
|
|
453
|
+
// Anzeigename der Quelle (E59): die Einstellungen-Liste zeigt den abgeleiteten Namen
|
|
454
|
+
// „skills-repo" statt der vollen Fixture-URL, das Namensfeld im Hinzufügen-Dialog gibt
|
|
455
|
+
// es zusätzlich zum URL/Pfad-Feld.
|
|
456
|
+
var quellenlisteNamen = Array.from(document.querySelectorAll('#quellenliste b'))
|
|
457
|
+
.map((b) => b.textContent)
|
|
458
|
+
.join(',');
|
|
459
|
+
var dqNameVorhanden = document.getElementById('dq-name') !== null;
|
|
460
|
+
// Umbenennen (E59): der Knopf je Quelle öffnet den Dialog mit dem aktuellen Namen im
|
|
461
|
+
// Feld, Abbrechen schließt ihn wieder. Beide Knöpfe der Zeile liegen zusammen in
|
|
462
|
+
// einem Kasten, sonst lägen sie im Grid übereinander.
|
|
463
|
+
var quellenknoepfeZahl = document.querySelectorAll('#quellenliste .quellenknoepfe').length;
|
|
464
|
+
var quelleUmbenennenFehler = null;
|
|
465
|
+
var dQuUOffenNachOeffnen = false;
|
|
466
|
+
var dQuUName = null;
|
|
467
|
+
var dQuUOffenNachAbbrechen = true;
|
|
468
|
+
try {
|
|
469
|
+
var dQuU = document.getElementById('d-quelle-umbenennen');
|
|
470
|
+
// E60: der Umbenennen-Knopf ist jetzt ein Stift-Icon-Knopf ohne Text (Klasse
|
|
471
|
+
// „umbenennen", aria-label statt Beschriftung).
|
|
472
|
+
document.querySelector('#quellenliste .umbenennen')?.click();
|
|
473
|
+
dQuUOffenNachOeffnen = dQuU?.open === true;
|
|
474
|
+
dQuUName = document.getElementById('dqu-name')?.value ?? null;
|
|
475
|
+
document.querySelector('#d-quelle-umbenennen button[value="abbruch"]')?.click();
|
|
476
|
+
dQuUOffenNachAbbrechen = dQuU?.open === true;
|
|
477
|
+
} catch (fehler) {
|
|
478
|
+
quelleUmbenennenFehler = String(fehler);
|
|
479
|
+
}
|
|
480
|
+
document.getElementById('d-einstellungen')?.close();
|
|
481
|
+
// Konfigzeile am Hermes-Ziel (E56): der verwaltete Pfad steht in der Karte, auch
|
|
482
|
+
// ungeklappt (hidden ändert textContent nicht). Dazu der Festlegen-Weg am unmanaged
|
|
483
|
+
// Profil, Öffnen und Abbrechen ohne Fehler, nach dem Muster der d-quelle-Prüfungen.
|
|
484
|
+
var konfigzeileEl = document.querySelector('.konfigzeile');
|
|
485
|
+
var konfigzeileText = konfigzeileEl ? konfigzeileEl.textContent.replace(/\\s+/g, ' ').trim() : null;
|
|
486
|
+
var skillOrdnerFehler = null;
|
|
487
|
+
var dSkillordnerOffenNachOeffnen = false;
|
|
488
|
+
var dSkillordnerOffenNachAbbrechen = true;
|
|
489
|
+
try {
|
|
490
|
+
var festlegenKnopf = Array.from(document.querySelectorAll('button'))
|
|
491
|
+
.find((b) => b.textContent === 'Skill-Ordner festlegen …');
|
|
492
|
+
festlegenKnopf?.click();
|
|
493
|
+
dSkillordnerOffenNachOeffnen = document.getElementById('d-skillordner')?.open === true;
|
|
494
|
+
document.querySelector('#d-skillordner button[value="abbruch"]')?.click();
|
|
495
|
+
dSkillordnerOffenNachAbbrechen = document.getElementById('d-skillordner')?.open === true;
|
|
496
|
+
} catch (fehler) {
|
|
497
|
+
skillOrdnerFehler = String(fehler);
|
|
498
|
+
}
|
|
499
|
+
// E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
|
|
500
|
+
// Zahnrad statt Sonne am Einstellungen-Knopf.
|
|
501
|
+
var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
|
|
502
|
+
// E55-Marken auch in der Ziel-Tabelle, unter der Beschreibung (2026-09-05).
|
|
503
|
+
var zielMarken = document.querySelector('#zielliste .linkliste .wasfeld .marken')?.textContent
|
|
504
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
505
|
+
var zielManuellVorhanden = (document.getElementById('zielliste')?.textContent ?? '').includes('manuell');
|
|
506
|
+
var zahnradVorhanden = (document.querySelector('#einstellungen svg')?.innerHTML ?? '').includes('6.47 3.56');
|
|
507
|
+
// Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
|
|
508
|
+
document.querySelector('[data-ansicht="katalog"]')?.click();
|
|
509
|
+
var katalogBeschreibung = document.querySelector('#katalog .zeile .beschreibung')?.textContent ?? null;
|
|
510
|
+
// Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
|
|
511
|
+
// einem Klick auf den Aufklapp-Knopf.
|
|
512
|
+
var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
|
|
513
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
514
|
+
var aufklapp = document.querySelector('#katalog .zeile .aufklappknopf');
|
|
515
|
+
var katalogDetailsZu = document.querySelector('#katalog .zeile .skillkoerper')?.hidden ?? null;
|
|
516
|
+
aufklapp?.click();
|
|
517
|
+
var katalogAufgeklappt = aufklapp?.getAttribute('aria-expanded') ?? null;
|
|
518
|
+
var katalogDetails = document.querySelector('#katalog .zeile .skillkoerper:not([hidden])')?.textContent
|
|
519
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
520
|
+
// Sicherheitsrelevante Marken (F11): Hooks und Werkzeuge tragen die warnende Klasse.
|
|
521
|
+
var katalogMarkenWarnend = Array.from(document.querySelectorAll('#katalog .zeile .merker.warnend'))
|
|
522
|
+
.map((m) => m.textContent).join(',');
|
|
523
|
+
// Skills-hinzufügen-Dialog (F13): dieselben Marken an der Wahlzeile.
|
|
524
|
+
var dskKnopf = Array.from(document.querySelectorAll('button'))
|
|
525
|
+
.find((b) => b.textContent === 'Skills hinzufügen');
|
|
526
|
+
var dskMarken = null;
|
|
527
|
+
if (dskKnopf) {
|
|
528
|
+
dskKnopf.click();
|
|
529
|
+
dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
|
|
530
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
531
|
+
document.getElementById('d-skills')?.close();
|
|
532
|
+
}
|
|
533
|
+
// MCP-Ansicht (E61-Folgerunde, F46: abgespeckt zum reinen Katalog): Navigationsreihenfolge
|
|
534
|
+
// Ziele, MCP, Plugins, Skills, Karten mit Name/Beschreibung/Homepage-Link, F39-Warnzeile,
|
|
535
|
+
// Einhängen-Dialog öffnen/abbrechen ohne IPC-Schreibaktion. Orte-Liste und Fremdkasten
|
|
536
|
+
// gehören seit F46 nicht mehr in diese Ansicht (die Prüfung darauf ist unten invertiert),
|
|
537
|
+
// sie leben nur noch am MCP-Abschnitt der Ziel-Karten (weiter unten unverändert geprüft).
|
|
538
|
+
var navReihenfolge = Array.from(document.querySelectorAll('.nav [data-ansicht]'))
|
|
539
|
+
.map((b) => b.dataset.ansicht).join(',');
|
|
540
|
+
document.querySelector('[data-ansicht="mcp"]')?.click();
|
|
541
|
+
var mcpKartenText = document.getElementById('mcpliste')?.textContent
|
|
542
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
543
|
+
var mcpHeimLinkVorhanden = document.querySelector('#mcpliste .mcpkopf .qlink') !== null;
|
|
544
|
+
var mcpWarnzeileVorhanden = document.querySelector('#mcpliste .mcp .ruhezeile') !== null;
|
|
545
|
+
// F46: die Orte-Liste je Karte und der Fremdkasten der Ansicht sind ganz weg, nicht nur
|
|
546
|
+
// leer — beide Selektoren dürfen im DOM überhaupt nicht mehr auftauchen.
|
|
547
|
+
var mcpOrteVorhanden = document.querySelector('#mcpliste .mcporte') !== null;
|
|
548
|
+
var mcpFremdkastenVorhanden = document.getElementById('mcpfremd') !== null;
|
|
549
|
+
var mcpFehler = null;
|
|
550
|
+
var dMcpOffenNachOeffnen = false;
|
|
551
|
+
var dMcpOffenNachAbbrechen = true;
|
|
552
|
+
// Die Wahlzeilen des Einhängen-Dialogs (F42): sie kommen aus der Adapter-Fähigkeit,
|
|
553
|
+
// hier steht, welche Orte zur Wahl stehen und wohin der Eintrag jeweils ginge. Zwei
|
|
554
|
+
// Karten, weil die Zeilen verschieden aussehen: „mail" hängt an beiden Home-Orten
|
|
555
|
+
// schon (gesperrte Zeile), „teamwork" nirgends (die Notizen je Ort).
|
|
556
|
+
var mcpWahlText = null;
|
|
557
|
+
var mcpWahlTextFrei = null;
|
|
558
|
+
try {
|
|
559
|
+
var mcpEinKnoepfe = Array.from(document.querySelectorAll('#mcpliste .mcpkopf button'))
|
|
560
|
+
.filter((b) => (b.textContent || '').indexOf('Einhängen') === 0);
|
|
561
|
+
var mcpWahlOeffnen = async (knopf) => {
|
|
562
|
+
knopf?.click();
|
|
563
|
+
// window.hmgr.mcpPlaetze() ist asynchron: kurz nachfassen, bis die Wahlzeilen stehen.
|
|
564
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
565
|
+
return document.getElementById('dm-orte')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
566
|
+
};
|
|
567
|
+
mcpWahlText = await mcpWahlOeffnen(mcpEinKnoepfe[0]);
|
|
568
|
+
dMcpOffenNachOeffnen = document.getElementById('d-mcp')?.open === true;
|
|
569
|
+
document.querySelector('#d-mcp button[value="abbruch"]')?.click();
|
|
570
|
+
dMcpOffenNachAbbrechen = document.getElementById('d-mcp')?.open === true;
|
|
571
|
+
mcpWahlTextFrei = await mcpWahlOeffnen(mcpEinKnoepfe[1]);
|
|
572
|
+
document.querySelector('#d-mcp button[value="abbruch"]')?.click();
|
|
573
|
+
} catch (fehler) {
|
|
574
|
+
mcpFehler = String(fehler);
|
|
575
|
+
}
|
|
576
|
+
// Aushängen-Rückfrage (F46: nur noch über den Entfernen-Knopf am MCP-Abschnitt einer
|
|
577
|
+
// Ziel-Karte auslösbar, die MCP-Ansicht selbst hat dafür keinen Knopf mehr).
|
|
578
|
+
var mcpAusFehler = null;
|
|
579
|
+
var dMcpAusOffenNachOeffnen = false;
|
|
580
|
+
var dMcpAusOffenNachAbbrechen = true;
|
|
581
|
+
// Seit E64 stehen im MCP-Abschnitt zwei Knopfarten nebeneinander (Entfernen und
|
|
582
|
+
// Übernehmen), deshalb wird hier nicht mehr der erste Knopf genommen, sondern der
|
|
583
|
+
// Entfernen-Knopf an seinem aria-label erkannt.
|
|
584
|
+
var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpkartezeilen button'));
|
|
585
|
+
try {
|
|
586
|
+
var mcpAusKnopf = mcpZeilenKnoepfe.find(function (b) {
|
|
587
|
+
return (b.getAttribute('aria-label') || '').indexOf('aushängen') > -1;
|
|
588
|
+
});
|
|
589
|
+
mcpAusKnopf?.click();
|
|
590
|
+
dMcpAusOffenNachOeffnen = document.getElementById('d-mcpaus')?.open === true;
|
|
591
|
+
document.querySelector('#d-mcpaus button[value="nein"]')?.click();
|
|
592
|
+
dMcpAusOffenNachAbbrechen = document.getElementById('d-mcpaus')?.open === true;
|
|
593
|
+
} catch (fehler) {
|
|
594
|
+
mcpAusFehler = String(fehler);
|
|
595
|
+
}
|
|
596
|
+
// Übernehmen aus der geteilten .mcp.json (E64): Knopf an einer geteilten Zeile,
|
|
597
|
+
// Rückfrage öffnen und über „Nein" wieder schließen — ohne IPC-Schreibaktion, wie bei
|
|
598
|
+
// den anderen MCP-Dialogen. Der Text der Rückfrage läuft mit, er ist die Warnung an
|
|
599
|
+
// den Nutzer und darf nicht stillschweigend verschwinden.
|
|
600
|
+
var mcpUebFehler = null;
|
|
601
|
+
var dMcpUebOffenNachOeffnen = false;
|
|
602
|
+
var dMcpUebOffenNachAbbrechen = true;
|
|
603
|
+
var mcpUebText = null;
|
|
604
|
+
try {
|
|
605
|
+
var mcpUebKnopf = mcpZeilenKnoepfe.find(function (b) {
|
|
606
|
+
return (b.textContent || '').indexOf('Übernehmen') === 0;
|
|
607
|
+
});
|
|
608
|
+
mcpUebKnopf?.click();
|
|
609
|
+
dMcpUebOffenNachOeffnen = document.getElementById('d-mcpueb')?.open === true;
|
|
610
|
+
mcpUebText = document.getElementById('dmu-text')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
611
|
+
document.querySelector('#d-mcpueb button[value="nein"]')?.click();
|
|
612
|
+
dMcpUebOffenNachAbbrechen = document.getElementById('d-mcpueb')?.open === true;
|
|
613
|
+
} catch (fehler) {
|
|
614
|
+
mcpUebFehler = String(fehler);
|
|
615
|
+
}
|
|
616
|
+
// MCP an der Ziel-Karte (E61-GUI-Nachtrag), Gegenrichtung zur MCP-Ansicht: Abschnitt und
|
|
617
|
+
// Knopf an der Hermes-Home-Karte (Katalog-Zeile mit Entfernen, Fremd-Zeile ohne Knopf)
|
|
618
|
+
// und an der kunde-x-Verzeichnis-Karte (zwei Zeilen aus der geteilten .mcp.json, seit
|
|
619
|
+
// E64 beide mit Übernehmen-Knopf), dazu die Knopf-Sichtbarkeit: an der agents-Karte
|
|
620
|
+
// darf weder Abschnitt noch Knopf stehen, agents implementiert die MCP-Fähigkeit
|
|
621
|
+
// nicht (E16).
|
|
622
|
+
var zielKarten = Array.from(document.querySelectorAll('#zielliste .ziel'));
|
|
623
|
+
var karteMitKopftext = function (t) {
|
|
624
|
+
return zielKarten.find(function (k) {
|
|
625
|
+
return (k.querySelector('.zielkopf')?.textContent || '').indexOf(t) > -1;
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
var mcpHinzuKnopfAn = function (karte) {
|
|
629
|
+
return karte
|
|
630
|
+
? Array.from(karte.querySelectorAll('button')).find((b) => (b.textContent || '').indexOf('MCP hinzufügen') === 0)
|
|
631
|
+
: undefined;
|
|
632
|
+
};
|
|
633
|
+
var hermesKarte = karteMitKopftext('Hermes · Home');
|
|
634
|
+
var hermesMcpText = hermesKarte?.querySelector('.mcpkarteabschnitt')?.textContent
|
|
635
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
636
|
+
var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
|
|
637
|
+
var hermesMcpKnopfVorhanden = mcpHinzuKnopfAn(hermesKarte) !== undefined;
|
|
638
|
+
var kundeXKarte = karteMitKopftext('kunde-x');
|
|
639
|
+
var kundeXMcpText = kundeXKarte?.querySelector('.mcpkarteabschnitt')?.textContent
|
|
640
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
641
|
+
var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
|
|
642
|
+
var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
|
|
643
|
+
var agentsKarte = karteMitKopftext('Agents · Home');
|
|
644
|
+
// Ohne diese Zusicherung wären die beiden Abwesenheits-Prüfungen darunter leer erfüllt,
|
|
645
|
+
// sobald die Karte selbst aus der Liste fiele: „kein Abschnitt" hieße dann nur „keine Karte".
|
|
646
|
+
var agentsKarteVorhanden = agentsKarte !== undefined;
|
|
647
|
+
var agentsMcpAbschnittVorhanden = (agentsKarte?.querySelector('.mcpkarteabschnitt') ?? null) !== null;
|
|
648
|
+
var agentsMcpKnopfVorhanden = mcpHinzuKnopfAn(agentsKarte) !== undefined;
|
|
649
|
+
// Dialog „MCP hinzufügen" (E47-Muster): öffnen an der Hermes-Karte und über Abbrechen
|
|
650
|
+
// wieder schließen, ohne IPC-Schreibaktion (mcpEinhaengen liefe erst nach „Einhängen").
|
|
651
|
+
var dmhFehler = null;
|
|
652
|
+
var dMcpHinzuOffenNachOeffnen = false;
|
|
653
|
+
var dMcpHinzuOffenNachAbbrechen = true;
|
|
654
|
+
var dmhListeText = null;
|
|
655
|
+
try {
|
|
656
|
+
mcpHinzuKnopfAn(hermesKarte)?.click();
|
|
657
|
+
dMcpHinzuOffenNachOeffnen = document.getElementById('d-mcp-hinzufuegen')?.open === true;
|
|
658
|
+
dmhListeText = document.getElementById('dmh-liste')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
659
|
+
document.querySelector('#d-mcp-hinzufuegen button[value="abbruch"]')?.click();
|
|
660
|
+
dMcpHinzuOffenNachAbbrechen = document.getElementById('d-mcp-hinzufuegen')?.open === true;
|
|
661
|
+
} catch (fehler) {
|
|
662
|
+
dmhFehler = String(fehler);
|
|
663
|
+
}
|
|
664
|
+
// Schriften aus fonts/ (Zusatzpunkt 6): erzwingt das Laden und belegt es danach.
|
|
665
|
+
await Promise.all([
|
|
666
|
+
document.fonts.load('400 16px Inter'),
|
|
667
|
+
document.fonts.load('500 16px "JetBrains Mono"'),
|
|
668
|
+
]);
|
|
669
|
+
return JSON.stringify({
|
|
670
|
+
titel: document.title,
|
|
671
|
+
kopfzahlen: document.getElementById('kopfzahlen')?.textContent,
|
|
672
|
+
zielliste: document.getElementById('zielliste')?.textContent?.replace(/\\s+/g, ' ').trim().slice(0, 600),
|
|
673
|
+
meldung: document.getElementById('meldungtext')?.textContent,
|
|
674
|
+
einstellungenOffen,
|
|
675
|
+
harnessSchalter,
|
|
676
|
+
zielBeschreibung,
|
|
677
|
+
zielMarken,
|
|
678
|
+
zielManuellVorhanden,
|
|
679
|
+
zahnradVorhanden,
|
|
680
|
+
katalogBeschreibung,
|
|
681
|
+
katalogMarken,
|
|
682
|
+
katalogDetailsZu,
|
|
683
|
+
katalogAufgeklappt,
|
|
684
|
+
katalogDetails,
|
|
685
|
+
katalogMarkenWarnend,
|
|
686
|
+
dskMarken,
|
|
687
|
+
interGeladen: document.fonts.check('400 16px Inter'),
|
|
688
|
+
monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
|
|
689
|
+
quelleneuVorhanden,
|
|
690
|
+
dialogeQuelleVorhanden,
|
|
691
|
+
quellenlisteNamen,
|
|
692
|
+
dqNameVorhanden,
|
|
693
|
+
quellenknoepfeZahl,
|
|
694
|
+
dQuUOffenNachOeffnen,
|
|
695
|
+
dQuUName,
|
|
696
|
+
dQuUOffenNachAbbrechen,
|
|
697
|
+
quelleUmbenennenFehler,
|
|
698
|
+
dQuelleOffenNachOeffnen,
|
|
699
|
+
dQuelleOffenNachAbbrechen,
|
|
700
|
+
quelleFehler,
|
|
701
|
+
dialogQuelleAufraeumenVorhanden,
|
|
702
|
+
dQuelleAufraeumenOffenNachOeffnen,
|
|
703
|
+
dQuelleAufraeumenOffenNachBehalten,
|
|
704
|
+
quelleAufraeumenFehler,
|
|
705
|
+
// Hinweiszeile im Erfolgsdialog (E26, F32): vorhanden und im Ruhezustand leer.
|
|
706
|
+
deHinweisVorhanden: document.getElementById('de-hinweis') !== null,
|
|
707
|
+
deHinweisVerborgen: document.getElementById('de-hinweis')?.hidden === true,
|
|
708
|
+
konfigzeileText,
|
|
709
|
+
dSkillordnerOffenNachOeffnen,
|
|
710
|
+
dSkillordnerOffenNachAbbrechen,
|
|
711
|
+
skillOrdnerFehler,
|
|
712
|
+
navReihenfolge,
|
|
713
|
+
mcpKartenText,
|
|
714
|
+
mcpHeimLinkVorhanden,
|
|
715
|
+
mcpWarnzeileVorhanden,
|
|
716
|
+
mcpOrteVorhanden,
|
|
717
|
+
mcpFremdkastenVorhanden,
|
|
718
|
+
dMcpOffenNachOeffnen,
|
|
719
|
+
dMcpOffenNachAbbrechen,
|
|
720
|
+
mcpWahlText,
|
|
721
|
+
mcpWahlTextFrei,
|
|
722
|
+
mcpFehler,
|
|
723
|
+
dMcpAusOffenNachOeffnen,
|
|
724
|
+
dMcpAusOffenNachAbbrechen,
|
|
725
|
+
mcpAusFehler,
|
|
726
|
+
dMcpUebOffenNachOeffnen,
|
|
727
|
+
dMcpUebOffenNachAbbrechen,
|
|
728
|
+
mcpUebText,
|
|
729
|
+
mcpUebFehler,
|
|
730
|
+
hermesMcpText,
|
|
731
|
+
hermesMcpZeilenKnopfZahl,
|
|
732
|
+
hermesMcpKnopfVorhanden,
|
|
733
|
+
kundeXMcpText,
|
|
734
|
+
kundeXMcpZeilenKnopfZahl,
|
|
735
|
+
kundeXMcpKnopfVorhanden,
|
|
736
|
+
agentsKarteVorhanden,
|
|
737
|
+
agentsMcpAbschnittVorhanden,
|
|
738
|
+
agentsMcpKnopfVorhanden,
|
|
739
|
+
dMcpHinzuOffenNachOeffnen,
|
|
740
|
+
dMcpHinzuOffenNachAbbrechen,
|
|
741
|
+
dmhListeText,
|
|
742
|
+
dmhFehler,
|
|
743
|
+
});
|
|
744
|
+
})()`)
|
|
745
|
+
.then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
|
|
746
|
+
.finally(() => app.quit());
|
|
747
|
+
}, 3000);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
// Selbsttätiges Aktualisieren (E44): einmal, sobald der Renderer bereit ist, danach alle sechs
|
|
751
|
+
// Stunden, solange das Fenster offen bleibt. Im Smoke-Test nie, sonst würde ein automatisierter
|
|
752
|
+
// Lauf einen echten Git-Fetch anstoßen.
|
|
753
|
+
if (!smoketest) {
|
|
754
|
+
const fenster = hauptfenster;
|
|
755
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
756
|
+
fenster.webContents.send('hmgr:aktualisierenAnstossen');
|
|
757
|
+
});
|
|
758
|
+
const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
|
|
759
|
+
fenster.on('closed', () => clearInterval(intervall));
|
|
760
|
+
}
|
|
761
|
+
// Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
|
|
762
|
+
// Beide stoßen nur ein stilles Neuladen der Anzeige an, nie einen Aktualisieren-Lauf mit
|
|
763
|
+
// Netzzugriff. Im Smoke-Test keins von beiden, dieselbe Zurückhaltung wie beim automatischen
|
|
764
|
+
// Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
|
|
765
|
+
if (!smoketest) {
|
|
766
|
+
const fenster = hauptfenster;
|
|
767
|
+
fenster.on('focus', () => neuLadenAnstossen());
|
|
768
|
+
void beobachtetePfade().then((pfade) => {
|
|
769
|
+
if (fenster.isDestroyed())
|
|
770
|
+
return; // Fenster schon zu, während die Pfade ermittelt wurden
|
|
771
|
+
beobachter = beobachten(pfade, () => neuLadenAnstossen(), { entprellenMs: BEOBACHTER_ENTPRELLEN_MS });
|
|
772
|
+
});
|
|
773
|
+
fenster.on('closed', () => {
|
|
774
|
+
beobachter?.close();
|
|
775
|
+
beobachter = undefined;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Holt das vorhandene Fenster nach vorn — die Antwort der laufenden Instanz auf einen zweiten
|
|
782
|
+
* Start (E67). Minimiert kommt es erst wieder hoch, dann bekommt es den Fokus.
|
|
783
|
+
*/
|
|
784
|
+
function fensterNachVornHolen() {
|
|
785
|
+
if (hauptfenster === null || hauptfenster.isDestroyed())
|
|
786
|
+
return;
|
|
787
|
+
if (hauptfenster.isMinimized())
|
|
788
|
+
hauptfenster.restore();
|
|
789
|
+
hauptfenster.show();
|
|
790
|
+
hauptfenster.focus();
|
|
791
|
+
}
|
|
792
|
+
registriereIpc();
|
|
793
|
+
if (einzelinstanz) {
|
|
794
|
+
app.on('second-instance', () => fensterNachVornHolen());
|
|
795
|
+
app.whenReady().then(() => {
|
|
796
|
+
// Im Smoke-Test kein Dock-Icon und kein Fokuswechsel: das Fenster bleibt unsichtbar (show:false
|
|
797
|
+
// oben), macOS würde aber ohne diese Zeile trotzdem ein Dock-Icon zeigen und aktivieren.
|
|
798
|
+
if (smoketest && process.platform === 'darwin')
|
|
799
|
+
app.dock?.hide();
|
|
800
|
+
// Windows-Fensterkonventionen (E67), beide nur dort:
|
|
801
|
+
// — Die Taskleiste gruppiert Fenster und Verknüpfungen über die AppUserModelID. Ohne sie steht
|
|
802
|
+
// der Prozess unter einer von Windows geratenen ID, und das angeheftete Startmenü-Symbol
|
|
803
|
+
// gehört dann nicht zum laufenden Fenster. Dieselbe ID wie die `appId` im build-Block, denn
|
|
804
|
+
// die schreibt der NSIS-Installer in die Verknüpfung.
|
|
805
|
+
// — Electron blendet unter Windows sonst sein Standardmenü ein (Datei/Bearbeiten/Ansicht/…),
|
|
806
|
+
// eine Menüleiste, die hmgr nicht bedient und die nur Platz über der Ansicht kostet. macOS
|
|
807
|
+
// bleibt unangetastet: dort ist die Menüleiste des Systems Pflicht (Beenden, Kopieren,
|
|
808
|
+
// Einfügen hängen daran).
|
|
809
|
+
if (process.platform === 'win32') {
|
|
810
|
+
app.setAppUserModelId('de.timkaufmann.hmgr');
|
|
811
|
+
Menu.setApplicationMenu(null);
|
|
812
|
+
}
|
|
813
|
+
fensterOeffnen();
|
|
814
|
+
app.on('activate', () => {
|
|
815
|
+
if (BrowserWindow.getAllWindows().length === 0)
|
|
816
|
+
fensterOeffnen();
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
app.on('window-all-closed', () => {
|
|
821
|
+
if (process.platform !== 'darwin')
|
|
822
|
+
app.quit();
|
|
823
|
+
});
|