hmgr 0.0.6 → 0.0.12

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/gui/main.js CHANGED
@@ -3,12 +3,16 @@
3
3
  // Fenster. Kein zweiter Prozess: der Kern läuft direkt im Main-Prozess, die GUI ist ein zweiter
4
4
  // Kompositions-Einstieg neben der CLI (ARCHITEKTUR.md „Eingang").
5
5
  import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron';
6
+ // electron-updater ist CommonJS: in ESM kommt nur der Default-Export an, `autoUpdater` wird aus ihm
7
+ // herausgegriffen. Ein benannter Import (`import { autoUpdater } from …`) scheitert zur Laufzeit.
8
+ import electronUpdater from 'electron-updater';
6
9
  import path from 'node:path';
7
10
  import { randomUUID } from 'node:crypto';
8
11
  import { stat } from 'node:fs/promises';
9
12
  import { fileURLToPath } from 'node:url';
10
13
  import { bauen, umgebungLesen } from "../cli/main.js";
11
14
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
15
+ import { paketVersion } from "../adapter/paket-version.js";
12
16
  import { quelleName, quelleSchluessel } from "../kern/modell.js";
13
17
  import { kannProfile } from "../ports/harness.js";
14
18
  import { iconFuerQuelle } from "./icons.js";
@@ -16,6 +20,8 @@ import { entscheideOeffnen } from "./oeffnen.js";
16
20
  import { mcpUebernahmeSerialisieren, mcpUebersichtSerialisieren, mcpWirkungSerialisieren } from "./mcp-ansicht.js";
17
21
  import { beobachten } from "./beobachter.js";
18
22
  import { loginPathUebernehmen } from "./login-pfad.js";
23
+ import { hmgrCliVersion } from "./hmgr-cli.js";
24
+ import { cliUpdateHinweis } from "../adapter/npm-registry.js";
19
25
  const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
20
26
  const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
21
27
  /**
@@ -117,6 +123,18 @@ const { anwendungsfaelle, ports } = bauen(umgebung, {
117
123
  rueckfrage,
118
124
  ...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
119
125
  });
126
+ /**
127
+ * Wer den laufenden Aktualisieren-Lauf ausgelöst hat (E80): der Renderer reicht es bei jedem Aufruf
128
+ * mit, hier gemerkt für die beiden hmgr-Selbstprüfungen (`update-downloaded`, `cliUpdatePruefen`),
129
+ * die selbst keinen eigenen Auslöser kennen. Default 'start', bevor der erste Lauf gemeldet hat.
130
+ */
131
+ let letzterAusloeser = 'start';
132
+ // Merkt die GUI-Version beim Start im Verlauf (E80): ein Versionssprung gegen den letzten Start
133
+ // bekommt einen Eintrag „hmgr-GUI X installiert". Läuft auch im Smoke-Test — hmgrHome zeigt dort auf
134
+ // ein Wegwerf-Verzeichnis (fixtureAufbauen in scripts/gui-smoke-test.ts), nie auf das echte ~/.hmgr.
135
+ void anwendungsfaelle.versionsSprungVermerken('gui', paketVersion(), 'start').catch((fehler) => {
136
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
137
+ });
120
138
  async function auffangen(fn) {
121
139
  await pathBereit; // erst der Login-PATH (F47=O2), dann der Adapter — sonst findet er die CLI nicht
122
140
  try {
@@ -136,6 +154,14 @@ function zielZustandSerialisieren(z) {
136
154
  gruende: Object.fromEntries(z.gruende),
137
155
  ignoriert: z.ignoriert,
138
156
  uebernahme: Object.fromEntries(z.uebernahme),
157
+ // Bundles des Ziels (E69): Adresse und Anzeigename der Quelle kommen mit, damit der Renderer
158
+ // die Gruppe beschriften kann, ohne die Quelle selbst nachzuschlagen.
159
+ bundles: z.bundles.map((b) => ({
160
+ name: b.name,
161
+ skills: b.skills,
162
+ quelleText: quelleAlsText(b.quelle),
163
+ quelleName: quelleName(b.quelle),
164
+ })),
139
165
  istRegistriert: z.istRegistriert,
140
166
  kannSkillOrdner: z.kannSkillOrdner,
141
167
  ...(z.plugins === undefined ? {} : { plugins: z.plugins }),
@@ -181,6 +207,7 @@ function registriereIpc() {
181
207
  ...(skill.modell === undefined ? {} : { modell: skill.modell }),
182
208
  ...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
183
209
  ...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
210
+ ...(skill.geaendert === undefined ? {} : { geaendert: skill.geaendert }),
184
211
  quelleText: quelleAlsText(eintrag.quelle),
185
212
  quelleName: quelleName(eintrag.quelle),
186
213
  quelleSchluessel: quelleSchluessel(eintrag.quelle),
@@ -189,6 +216,45 @@ function registriereIpc() {
189
216
  }
190
217
  return ergebnis;
191
218
  }));
219
+ // Bundles aller bekannten Quellen (E69): flach wie der Skill-Katalog, je Bundle seine Quelle und
220
+ // die Zahl seiner Mitglieder. Kaputte Definitionen kommen als eigene Liste mit, damit die GUI sie
221
+ // nennen kann, statt sie zu verschlucken.
222
+ ipcMain.handle('hmgr:bundles', async () => auffangen(async () => {
223
+ const bundles = [];
224
+ const kaputte = [];
225
+ for (const eintrag of await anwendungsfaelle.bundleKatalog()) {
226
+ for (const bundle of eintrag.bundles)
227
+ bundles.push({
228
+ name: bundle.name,
229
+ beschreibung: bundle.beschreibung,
230
+ mitglieder: bundle.mitglieder.length,
231
+ // Anzeige im Dialog: aus welchen Quellen das Bundle Skills holt, ohne Dubletten.
232
+ quellen: [...new Set(bundle.mitglieder.map((m) => quelleAlsText(m.quelle)))],
233
+ quelleText: quelleAlsText(bundle.quelle),
234
+ quelleName: quelleName(bundle.quelle),
235
+ });
236
+ for (const kaputt of eintrag.kaputte)
237
+ kaputte.push({ quelleName: quelleName(eintrag.quelle), datei: kaputt.datei, fehler: kaputt.fehler });
238
+ }
239
+ return { bundles, kaputte };
240
+ }));
241
+ // Installieren und Entfernen wirken auf das Bundle als Ganzes (E69). Der Zeiger ist nicht im
242
+ // Spiel, die Zugehörigkeit steht im Zustand — anders als bei linkLegen braucht es kein `lokal`.
243
+ ipcMain.handle('hmgr:bundleInstallieren', async (_event, args) => auffangen(async () => {
244
+ const ergebnis = await anwendungsfaelle.bundleInstallieren({
245
+ bundle: args.bundle,
246
+ ziel: zielAusIpc(args.harness, args.ort),
247
+ ...(args.quelle === undefined ? {} : { quelle: args.quelle }),
248
+ });
249
+ return {
250
+ name: ergebnis.bundle.name,
251
+ gelegt: ergebnis.gelegt,
252
+ uebernommen: ergebnis.uebernommen,
253
+ neueQuellen: ergebnis.neueQuellen.map((q) => quelleName(q)),
254
+ ...(ergebnis.hinweis === undefined ? {} : { hinweis: ergebnis.hinweis }),
255
+ };
256
+ }));
257
+ ipcMain.handle('hmgr:bundleEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.bundleEntfernen({ bundle: args.bundle, ziel: zielAusIpc(args.harness, args.ort) })));
192
258
  // Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
193
259
  ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
194
260
  art: quelle.art,
@@ -247,7 +313,25 @@ function registriereIpc() {
247
313
  lokal: args.ort.art === 'arbeitsverzeichnis',
248
314
  })));
249
315
  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)));
316
+ ipcMain.handle('hmgr:aktualisieren', async (_event, args) => auffangen(async () => {
317
+ if (args?.ausloeser !== undefined)
318
+ letzterAusloeser = args.ausloeser;
319
+ return anwendungsfaelle.aktualisieren(args);
320
+ }));
321
+ // Changelog der letzten Läufe (E80), neueste zuerst.
322
+ ipcMain.handle('hmgr:verlauf', async () => auffangen(async () => anwendungsfaelle.verlauf()));
323
+ // E77: der Prüfen-Knopf stößt zusätzlich zum Quellen-Lauf die Prüfung auf hmgr selbst an. Kein
324
+ // `handle`, sondern `send`/`on`: der Renderer wartet auf nichts, das Ergebnis kommt später von
325
+ // sich aus über 'hmgr:selbstUpdate'.
326
+ ipcMain.on('hmgr:selbstUpdatePruefen', () => selbstUpdatePruefen());
327
+ // Neustart-Knopf an der GUI-Update-Zeile (E80): installiert das geladene Paket sofort, statt auf
328
+ // das nächste Beenden zu warten. Im Smoke-Test ein No-op — dort läuft `updatePruefen` ohnehin nie,
329
+ // der Knopf bleibt verborgen, aber ein Absturz durch ein fehlendes Update darf trotzdem nie sein.
330
+ ipcMain.on('hmgr:neuStarten', () => {
331
+ if (smoketest)
332
+ return;
333
+ autoUpdater.quitAndInstall();
334
+ });
251
335
  ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
252
336
  ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
253
337
  // Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). Im Smoke-Test darf
@@ -270,6 +354,14 @@ function registriereIpc() {
270
354
  ipcMain.handle('hmgr:harnessAktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessAktivieren(name)));
271
355
  ipcMain.handle('hmgr:harnessDeaktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessDeaktivieren(name)));
272
356
  ipcMain.handle('hmgr:saeubern', async (_event, name) => auffangen(async () => anwendungsfaelle.saeubern(name)));
357
+ // Homedirs je Harness (E78): zeigen, das primäre umstellen (F89), zusätzliche in einer
358
+ // WSL-Distro eintragen und vergessen (F87). Die Distro-Liste kommt aus `wsl -l -q`.
359
+ ipcMain.handle('hmgr:harnessHeime', async () => auffangen(async () => anwendungsfaelle.harnessHeime()));
360
+ ipcMain.handle('hmgr:wslDistros', async () => auffangen(async () => anwendungsfaelle.wslDistros()));
361
+ ipcMain.handle('hmgr:harnessHeimSetzen', async (_event, args) => auffangen(async () => anwendungsfaelle.harnessHeimSetzen(args.harness, args.pfad)));
362
+ ipcMain.handle('hmgr:harnessHeimZuruecksetzen', async (_event, harness) => auffangen(async () => anwendungsfaelle.harnessHeimZuruecksetzen(harness)));
363
+ ipcMain.handle('hmgr:harnessHeimHinzufuegen', async (_event, args) => auffangen(async () => anwendungsfaelle.harnessHeimHinzufuegen(args.harness, args.adresse)));
364
+ ipcMain.handle('hmgr:harnessHeimEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.harnessHeimEntfernen(args.harness, args.adresse)));
273
365
  // Verwalteter Hermes-Skill-Ordner (E56): Zeigen kommt schon über hmgr:istZustand, hier nur Ändern.
274
366
  ipcMain.handle('hmgr:skillOrdnerFestlegen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerFestlegen(zielAusIpc(args.harness, args.ort), args.pfad)));
275
367
  ipcMain.handle('hmgr:skillOrdnerEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerEntfernen(zielAusIpc(args.harness, args.ort))));
@@ -386,6 +478,11 @@ function fensterOeffnen() {
386
478
  minWidth: 860,
387
479
  minHeight: 560,
388
480
  backgroundColor: '#E3E7E0', // --platte, verhindert einen weißen Blitz beim Start
481
+ // E72: der Fenstertitel nennt die Version, damit im Feld erkennbar ist, welcher Stand läuft.
482
+ // Nicht `app.getVersion()`: das liefert nur im gepackten Bundle die Paketversion, beim Start
483
+ // über `electron dist/gui/main.js` (npm run gui, Smoke-Test) dagegen die Electron-Version —
484
+ // dort liegt neben dem Hauptskript keine package.json (gemessen: 44.0.0 statt 0.0.6).
485
+ title: `hmgr ${paketVersion()}`,
389
486
  // Im Smoke-Test bleibt das Fenster unsichtbar: der Renderer lädt trotzdem vollständig, der
390
487
  // DOM-Auszug kommt per executeJavaScript. So klaut ein automatisierter Lauf niemandem den
391
488
  // Eingabefokus und öffnet kein sichtbares Fenster.
@@ -400,6 +497,9 @@ function fensterOeffnen() {
400
497
  hauptfenster.on('closed', () => {
401
498
  hauptfenster = null;
402
499
  });
500
+ // Der <title> der index.html setzt den Fenstertitel beim Laden sonst zurück und überschriebe die
501
+ // Option oben (E72). Der DOM-Titel bleibt dabei, wie er ist, nur das Fenster folgt ihm nicht mehr.
502
+ hauptfenster.on('page-title-updated', (ereignis) => ereignis.preventDefault());
403
503
  // Renderer-Konsole im Main-Log, für Fehlersuche und den Smoke-Test.
404
504
  hauptfenster.webContents.on('console-message', (event) => {
405
505
  console.log(`[renderer] ${event.message}`);
@@ -417,6 +517,25 @@ function fensterOeffnen() {
417
517
  var harnessSchalter = Array.from(document.querySelectorAll('#harnessliste input[type=checkbox]'))
418
518
  .map((i) => i.dataset.harness + ':' + i.checked)
419
519
  .join(',');
520
+ // Homedir-Zeilen unter den Schaltern (E78): je Harness eine Zeile mit dem Pfad, dazu
521
+ // der Ändern-Knopf. Der Weg zu einem WSL-Homedir steht auf dem Mac bewusst nicht da.
522
+ var heimZeilen = Array.from(document.querySelectorAll('#harnessliste .heimzeile'))
523
+ .map((z) => z.textContent.replace(/\\s+/g, ' ').trim())
524
+ .join(' | ');
525
+ var heimWslKnopfVorhanden = Array.from(document.querySelectorAll('#harnessliste .heime > button'))
526
+ .some((b) => b.textContent.indexOf('WSL') !== -1);
527
+ // Der Pfad-Dialog lässt sich öffnen und per Abbrechen wieder schließen, ohne IPC-Lauf.
528
+ var heimPfadFehler = null;
529
+ var dHeimPfadOffenNachOeffnen = false;
530
+ var dHeimPfadOffenNachAbbrechen = true;
531
+ try {
532
+ document.querySelector('#harnessliste .heimknoepfe button')?.click();
533
+ dHeimPfadOffenNachOeffnen = document.getElementById('d-heim-pfad')?.open === true;
534
+ document.querySelector('#d-heim-pfad button[value="abbruch"]')?.click();
535
+ dHeimPfadOffenNachAbbrechen = document.getElementById('d-heim-pfad')?.open === true;
536
+ } catch (fehler) {
537
+ heimPfadFehler = String(fehler);
538
+ }
420
539
  // Quellen-Abschnitt der Einstellungen (E54): Knopf und Dialoge vorhanden, der
421
540
  // Hinzufügen-Dialog lässt sich öffnen und per Abbrechen ohne Fehler wieder schließen.
422
541
  // Kein echter Quellen-Lauf: der Abbrechen-Knopf löst keinen IPC-Aufruf aus.
@@ -477,6 +596,17 @@ function fensterOeffnen() {
477
596
  } catch (fehler) {
478
597
  quelleUmbenennenFehler = String(fehler);
479
598
  }
599
+ // Leertext der Quellenliste (E75): der Smoke-Lauf hat immer eine Quelle, den leeren
600
+ // Zustand erreicht er nie. Geprüft wird darum die Regel, an der er zerbrach — ein
601
+ // <li class="leerhinweis"> in genau dieser Liste ist vom Grid ausgenommen und nimmt
602
+ // die ganze Breite, statt in die 20px-Icon-Spalte zu fallen und Wort für Wort
603
+ // umzubrechen. Die Probe wird sofort wieder entfernt, die Liste bleibt unberührt.
604
+ var quellenLeerProbe = document.createElement('li');
605
+ quellenLeerProbe.className = 'leerhinweis';
606
+ quellenLeerProbe.textContent = 'Probe';
607
+ document.getElementById('quellenliste')?.appendChild(quellenLeerProbe);
608
+ var quellenLeerDisplay = getComputedStyle(quellenLeerProbe).display;
609
+ quellenLeerProbe.remove();
480
610
  document.getElementById('d-einstellungen')?.close();
481
611
  // Konfigzeile am Hermes-Ziel (E56): der verwaltete Pfad steht in der Karte, auch
482
612
  // ungeklappt (hidden ändert textContent nicht). Dazu der Festlegen-Weg am unmanaged
@@ -496,6 +626,52 @@ function fensterOeffnen() {
496
626
  } catch (fehler) {
497
627
  skillOrdnerFehler = String(fehler);
498
628
  }
629
+ // E77: der Ort für die Update-Hinweise steht in der Seitenleiste, direkt vor dem
630
+ // Einstellungen-Knopf, und ist im Ruhezustand leer und verborgen. Der Smoke-Lauf
631
+ // prüft nie nach — er stößt weder Updater noch CLI-Prüfung an, es gibt nichts zu
632
+ // melden. Geprüft wird darum nur, dass der Ort da ist und stumm bleibt.
633
+ var selbstupdateEl = document.getElementById('selbstupdate');
634
+ var selbstupdateVorhanden = selbstupdateEl !== null;
635
+ // Nicht nur das Attribut, sondern das tatsächliche Rendern: die Klasse setzt
636
+ // display:flex, und eine Autoren-Regel überstimmt das hidden-Attribut des Browsers —
637
+ // ohne die eigene hidden-Regel in styles.css stünde hier ein leerer Kasten samt
638
+ // seinem auto-Rand. Das Attribut allein würde den Fehler nicht zeigen.
639
+ var selbstupdateVerborgen =
640
+ selbstupdateEl?.hidden === true && getComputedStyle(selbstupdateEl).display === 'none';
641
+ // Seit E80 steckt im Kasten auch der Neustart-Knopf, sein Text bleibt aber im DOM
642
+ // stehen, auch verborgen (anders als bei display:none zählt hidden nicht für
643
+ // textContent) — „leer" meint deshalb: kein sichtbarer (nicht verborgener) Kindknoten
644
+ // trägt Text, nicht der rohe textContent des ganzen Kastens.
645
+ var selbstupdateLeer = Array.from(selbstupdateEl?.children ?? [])
646
+ .filter((kind) => !kind.hidden)
647
+ .every((kind) => (kind.textContent ?? '').trim() === '');
648
+ // Er sitzt zwischen Stand-Zeile und Einstellungen-Knopf: der Knopf ist sein nächster
649
+ // Geschwister-Knoten, sonst stünde der Hinweis irgendwo anders in der Leiste.
650
+ var selbstupdateVorEinstellungen = selbstupdateEl?.nextElementSibling?.id === 'einstellungen';
651
+ // E80: Neustart-Knopf an der GUI-Update-Zeile, vorhanden und im Ruhezustand verborgen
652
+ // (der Smoke-Lauf löst nie ein update-downloaded aus, das ihn einblenden würde).
653
+ var selbstupdateNeustartVorhanden = document.getElementById('selbstupdate-neustart') !== null;
654
+ var selbstupdateNeustartVerborgen = document.getElementById('selbstupdate-neustart')?.hidden === true;
655
+ // E80: die Seitenleiste zeigt nur noch die Stand-Zeile und ein „›" zum Öffnen der
656
+ // Ansicht, kein „Prüfen"-Wort mehr und kein eigener Lauf am Standknopf-Klick. In der
657
+ // Ansicht selbst ersetzt ein runder Kringel den früheren großen Knopf, und der alte
658
+ // Sitzungsbericht (#bericht) ist ganz weg, ersetzt durch das Changelog (#verlauf).
659
+ var standknopfText = document.getElementById('standknopf')?.textContent ?? null;
660
+ var updatestartKringel = document.getElementById('updatestart')?.classList.contains('kringel') === true;
661
+ var updatestartLaeuft = document.getElementById('updatestart')?.classList.contains('laeuft') === true;
662
+ var berichtVorhanden = document.getElementById('bericht') !== null;
663
+ var verlaufVorhanden = document.getElementById('verlauf') !== null;
664
+ // Der Standknopf öffnet nur noch die Ansicht (kein Lauf mehr an diesem Klick), die
665
+ // Ansicht lädt ihren Verlauf dabei aus der Datei — das ist ein IPC-Aufruf, also kurz
666
+ // nachfassen, bis das Ergebnis im DOM steht (Muster wie bei mcpWahlOeffnen oben).
667
+ document.getElementById('standknopf')?.click();
668
+ var verlaufBloecke = 0;
669
+ for (var wartezeit = 0; wartezeit < 2000; wartezeit += 100) {
670
+ await new Promise((r) => setTimeout(r, 100));
671
+ verlaufBloecke = document.querySelectorAll('#verlauf .block').length;
672
+ if (verlaufBloecke > 0) break;
673
+ }
674
+ var verlaufText = document.getElementById('verlauf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
499
675
  // E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
500
676
  // Zahnrad statt Sonne am Einstellungen-Knopf.
501
677
  var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
@@ -507,6 +683,60 @@ function fensterOeffnen() {
507
683
  // Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
508
684
  document.querySelector('[data-ansicht="katalog"]')?.click();
509
685
  var katalogBeschreibung = document.querySelector('#katalog .zeile .beschreibung')?.textContent ?? null;
686
+ // E70/F70=O1: die Herkunftszeile trägt hinter dem Ref das Änderungsdatum des Skills.
687
+ var katalogHerkunft = document.querySelector('#katalog .zeile .herkunft')?.textContent
688
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
689
+ // E79: Sortier-Gruppe der Skills-Rubrik. Erst „Zuletzt geändert" klicken und die
690
+ // sichtbaren Änderungsdaten der ersten fünf Zeilen samt Knopfstand festhalten, dann
691
+ // zurück auf „Name", damit die obigen Prüfungen (erste Zeile) unverändert gelten.
692
+ document.getElementById('sortnach-datum')?.click();
693
+ var katalogSortierungGeaendert = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden])'))
694
+ .slice(0, 5)
695
+ .map((li) => li.dataset.geaendert ?? '')
696
+ .join('|');
697
+ var katalogSortierungDatumGedrueckt = document.getElementById('sortnach-datum')?.getAttribute('aria-pressed') ?? null;
698
+ var katalogSortierungNameGedrueckt = document.getElementById('sortnach-name')?.getAttribute('aria-pressed') ?? null;
699
+ // E81: Richtung und Pfeil nach dem ersten Klick. Standardrichtung von „datum" ist
700
+ // 'ab' (jüngste zuerst, wie E79), der Name-Chip ist dabei inaktiv und trägt keinen
701
+ // Pfeil.
702
+ var katalogSortierungDatumRichtung = document.getElementById('sortnach-datum')?.getAttribute('data-richtung') ?? null;
703
+ var katalogSortierungDatumPfeil = document.querySelector('#sortnach-datum .sortpfeil')?.textContent ?? null;
704
+ var katalogSortierungNamePfeil = document.querySelector('#sortnach-name .sortpfeil')?.textContent ?? null;
705
+ // E81: zweiter Klick auf denselben Chip kehrt nur die Richtung um (Schlüssel bleibt
706
+ // „datum", jetzt älteste zuerst).
707
+ document.getElementById('sortnach-datum')?.click();
708
+ var katalogSortierungGeaendertAuf = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden])'))
709
+ .slice(0, 5)
710
+ .map((li) => li.dataset.geaendert ?? '')
711
+ .join('|');
712
+ var katalogSortierungDatumRichtungAuf = document.getElementById('sortnach-datum')?.getAttribute('data-richtung') ?? null;
713
+ var katalogSortierungDatumPfeilAuf = document.querySelector('#sortnach-datum .sortpfeil')?.textContent ?? null;
714
+ document.getElementById('sortnach-name')?.click();
715
+ // Reiner Textknoten des Namens (erstes Kind ist das Quellen-Icon, ein etwaiges
716
+ // Markenfeld käme als weiteres Element danach) statt h3.textContent, sonst würde
717
+ // eine Marke direkt an den Namen geklebt.
718
+ var ersterKatalogH3 = document.querySelector('#katalog .zeile:not([hidden]) h3');
719
+ var katalogErsterNachName = ersterKatalogH3
720
+ ? Array.from(ersterKatalogH3.childNodes).find((n) => n.nodeType === 3)?.textContent ?? null
721
+ : null;
722
+ // E81: Chipwechsel (anderer Schlüssel als zuvor „datum") setzt dessen Standardrichtung,
723
+ // hier 'auf' für Name — die bisherige, unveränderte Reihenfolge (A→Z).
724
+ var katalogNamenAuf = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden]) h3'))
725
+ .map((h3) => Array.from(h3.childNodes).find((n) => n.nodeType === 3)?.textContent ?? '')
726
+ .join('|');
727
+ var katalogSortierungNameRichtungAuf = document.getElementById('sortnach-name')?.getAttribute('data-richtung') ?? null;
728
+ var katalogSortierungNamePfeilAuf = document.querySelector('#sortnach-name .sortpfeil')?.textContent ?? null;
729
+ // E81: zweiter Klick auf „Name" kehrt die Richtung um (Z→A).
730
+ document.getElementById('sortnach-name')?.click();
731
+ var katalogNamenAb = Array.from(document.querySelectorAll('#katalog .zeile:not([hidden]) h3'))
732
+ .map((h3) => Array.from(h3.childNodes).find((n) => n.nodeType === 3)?.textContent ?? '')
733
+ .join('|');
734
+ var katalogSortierungNameRichtungAb = document.getElementById('sortnach-name')?.getAttribute('data-richtung') ?? null;
735
+ var katalogSortierungNamePfeilAb = document.querySelector('#sortnach-name .sortpfeil')?.textContent ?? null;
736
+ // E81: dritter Klick stellt name/auf wieder her, damit die restliche Sonde (unten)
737
+ // wie gewohnt gegen die Standardsortierung prüft.
738
+ document.getElementById('sortnach-name')?.click();
739
+ var katalogSortierungNameRichtungNachDrittemKlick = document.getElementById('sortnach-name')?.getAttribute('data-richtung') ?? null;
510
740
  // Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
511
741
  // einem Klick auf den Aufklapp-Knopf.
512
742
  var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
@@ -524,12 +754,45 @@ function fensterOeffnen() {
524
754
  var dskKnopf = Array.from(document.querySelectorAll('button'))
525
755
  .find((b) => b.textContent === 'Skills hinzufügen');
526
756
  var dskMarken = null;
757
+ // Bundle-Sektion im selben Dialog (E69, F68): sie steht dort, wo die Entscheidung
758
+ // fällt, was ins Ziel kommt, und zeigt je Bundle Beschreibung und Herkunft.
759
+ var dskBundleBoxSichtbar = false;
760
+ var dskBundleText = null;
527
761
  if (dskKnopf) {
528
762
  dskKnopf.click();
529
763
  dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
530
764
  ?.replace(/\\s+/g, ' ').trim() ?? null;
765
+ dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
766
+ dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
767
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
531
768
  document.getElementById('d-skills')?.close();
532
769
  }
770
+ // Verlinken-Dialog „Wohin?" (E76): er bietet nur Ziele aktiver Harnesses an. Der Text
771
+ // der Wahlliste sagt, welche das sind; ist gar keines aktiv, steht dort der Leerhinweis.
772
+ var dzZieleText = null;
773
+ var dzKnopf = Array.from(document.querySelectorAll('#katalog .zeile button'))
774
+ .find((b) => b.textContent === 'Verlinken');
775
+ if (dzKnopf) {
776
+ dzKnopf.click();
777
+ dzZieleText = document.getElementById('dz-ziele')?.textContent
778
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
779
+ document.getElementById('d-ziel')?.close();
780
+ }
781
+ // Bundle-Gruppe an der Ziel-Karte (E69): Kopf mit Name, Quelle und Entfernen, darunter
782
+ // die Zeilen der Mitglieder — sie stehen in der Gruppe, nicht in der allgemeinen Liste.
783
+ var bundleGruppeText = document.querySelector('#zielliste .bundlegruppe .bundlekopf')?.textContent
784
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
785
+ var bundleGruppeZeilen = Array.from(document.querySelectorAll('#zielliste .bundlegruppe .linkliste .name'))
786
+ .map((n) => n.textContent.trim()).join(',');
787
+ // Entfernen an der Einzelzeile innerhalb der Gruppe sagt, dass das Bundle zerbricht.
788
+ var bundleZerbrichtText = null;
789
+ try {
790
+ document.querySelector('#zielliste .bundlegruppe .linkliste .knopf')?.click();
791
+ bundleZerbrichtText = document.getElementById('dx-text')?.textContent ?? null;
792
+ document.querySelector('#d-entfernen button[value="nein"]')?.click();
793
+ } catch (fehler) {
794
+ bundleZerbrichtText = String(fehler);
795
+ }
533
796
  // MCP-Ansicht (E61-Folgerunde, F46: abgespeckt zum reinen Katalog): Navigationsreihenfolge
534
797
  // Ziele, MCP, Plugins, Skills, Karten mit Name/Beschreibung/Homepage-Link, F39-Warnzeile,
535
798
  // Einhängen-Dialog öffnen/abbrechen ohne IPC-Schreibaktion. Orte-Liste und Fremdkasten
@@ -640,12 +903,33 @@ function fensterOeffnen() {
640
903
  ?.replace(/\\s+/g, ' ').trim() ?? null;
641
904
  var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
642
905
  var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
906
+ // Kartenkopf-Zahl (E71): der Kopf zählt Skills, gebündelt wie der Körper zeilt. An der
907
+ // Verzeichnis-Karte kunde-x hängen dieselben zwei Skills an zwei Harnesses, eine Summe
908
+ // über die Abschnitte ergäbe hier 4.
909
+ var kopfZahlVon = function (karte) {
910
+ return karte?.querySelector('.zielkopf .zahl')?.textContent?.trim() ?? null;
911
+ };
912
+ var kundeXKopfZahl = kopfZahlVon(kundeXKarte);
913
+ var hermesKopfZahl = kopfZahlVon(hermesKarte);
643
914
  var agentsKarte = karteMitKopftext('Agents · Home');
644
915
  // Ohne diese Zusicherung wären die beiden Abwesenheits-Prüfungen darunter leer erfüllt,
645
916
  // sobald die Karte selbst aus der Liste fiele: „kein Abschnitt" hieße dann nur „keine Karte".
646
917
  var agentsKarteVorhanden = agentsKarte !== undefined;
647
918
  var agentsMcpAbschnittVorhanden = (agentsKarte?.querySelector('.mcpkarteabschnitt') ?? null) !== null;
648
919
  var agentsMcpKnopfVorhanden = mcpHinzuKnopfAn(agentsKarte) !== undefined;
920
+ // Warnzahl am Kartenkopf (E75): das Dreieck nennt die Zahl der Auslöser. Im Fixture
921
+ // liegt ein fremder Ordner im Hermes-Home-Ziel (1) und drei im Verzeichnis kunde-x,
922
+ // ungleich auf die zwei Harness-Teile verteilt — dort ist die Zahl die Summe. Die
923
+ // agents-Karte hat keinen Auslöser und darum gar kein Dreieck.
924
+ var warnungVon = function (karte) { return karte?.querySelector('.zielkopf .warnung') ?? null; };
925
+ var warnzahlVon = function (karte) {
926
+ return warnungVon(karte)?.querySelector('.warnzahl')?.textContent?.trim() ?? null;
927
+ };
928
+ var hermesWarnzahl = warnzahlVon(hermesKarte);
929
+ var hermesWarnlabel = warnungVon(hermesKarte)?.getAttribute('aria-label') ?? null;
930
+ var kundeXWarnzahl = warnzahlVon(kundeXKarte);
931
+ var kundeXWarnlabel = warnungVon(kundeXKarte)?.getAttribute('aria-label') ?? null;
932
+ var agentsWarnungVorhanden = warnungVon(agentsKarte) !== null;
649
933
  // Dialog „MCP hinzufügen" (E47-Muster): öffnen an der Hermes-Karte und über Abbrechen
650
934
  // wieder schließen, ohne IPC-Schreibaktion (mcpEinhaengen liefe erst nach „Einhängen").
651
935
  var dmhFehler = null;
@@ -673,17 +957,46 @@ function fensterOeffnen() {
673
957
  meldung: document.getElementById('meldungtext')?.textContent,
674
958
  einstellungenOffen,
675
959
  harnessSchalter,
960
+ heimZeilen,
961
+ heimWslKnopfVorhanden,
962
+ heimPfadFehler,
963
+ dHeimPfadOffenNachOeffnen,
964
+ dHeimPfadOffenNachAbbrechen,
676
965
  zielBeschreibung,
677
966
  zielMarken,
678
967
  zielManuellVorhanden,
679
968
  zahnradVorhanden,
680
969
  katalogBeschreibung,
970
+ katalogHerkunft,
971
+ katalogSortierungGeaendert,
972
+ katalogSortierungDatumGedrueckt,
973
+ katalogSortierungNameGedrueckt,
974
+ katalogSortierungDatumRichtung,
975
+ katalogSortierungDatumPfeil,
976
+ katalogSortierungNamePfeil,
977
+ katalogSortierungGeaendertAuf,
978
+ katalogSortierungDatumRichtungAuf,
979
+ katalogSortierungDatumPfeilAuf,
980
+ katalogErsterNachName,
981
+ katalogNamenAuf,
982
+ katalogSortierungNameRichtungAuf,
983
+ katalogSortierungNamePfeilAuf,
984
+ katalogNamenAb,
985
+ katalogSortierungNameRichtungAb,
986
+ katalogSortierungNamePfeilAb,
987
+ katalogSortierungNameRichtungNachDrittemKlick,
681
988
  katalogMarken,
682
989
  katalogDetailsZu,
683
990
  katalogAufgeklappt,
684
991
  katalogDetails,
685
992
  katalogMarkenWarnend,
686
993
  dskMarken,
994
+ dskBundleBoxSichtbar,
995
+ dskBundleText,
996
+ dzZieleText,
997
+ bundleGruppeText,
998
+ bundleGruppeZeilen,
999
+ bundleZerbrichtText,
687
1000
  interGeladen: document.fonts.check('400 16px Inter'),
688
1001
  monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
689
1002
  quelleneuVorhanden,
@@ -733,6 +1046,14 @@ function fensterOeffnen() {
733
1046
  kundeXMcpText,
734
1047
  kundeXMcpZeilenKnopfZahl,
735
1048
  kundeXMcpKnopfVorhanden,
1049
+ kundeXKopfZahl,
1050
+ hermesKopfZahl,
1051
+ hermesWarnzahl,
1052
+ hermesWarnlabel,
1053
+ kundeXWarnzahl,
1054
+ kundeXWarnlabel,
1055
+ agentsWarnungVorhanden,
1056
+ quellenLeerDisplay,
736
1057
  agentsKarteVorhanden,
737
1058
  agentsMcpAbschnittVorhanden,
738
1059
  agentsMcpKnopfVorhanden,
@@ -740,22 +1061,39 @@ function fensterOeffnen() {
740
1061
  dMcpHinzuOffenNachAbbrechen,
741
1062
  dmhListeText,
742
1063
  dmhFehler,
1064
+ selbstupdateVorhanden,
1065
+ selbstupdateVerborgen,
1066
+ selbstupdateLeer,
1067
+ selbstupdateVorEinstellungen,
1068
+ selbstupdateNeustartVorhanden,
1069
+ selbstupdateNeustartVerborgen,
1070
+ standknopfText,
1071
+ updatestartKringel,
1072
+ updatestartLaeuft,
1073
+ berichtVorhanden,
1074
+ verlaufVorhanden,
1075
+ verlaufText,
1076
+ verlaufBloecke,
743
1077
  });
744
1078
  })()`)
745
1079
  .then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
1080
+ // E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
1081
+ // index.html), er lässt sich nur hier im Main-Prozess ablesen.
1082
+ .then(() => console.log(`[smoke] Fenster: ${JSON.stringify({ fenstertitel: fenster.getTitle() })}`))
746
1083
  .finally(() => app.quit());
747
1084
  }, 3000);
748
1085
  });
749
1086
  }
750
1087
  // Selbsttätiges Aktualisieren (E44): einmal, sobald der Renderer bereit ist, danach alle sechs
751
1088
  // Stunden, solange das Fenster offen bleibt. Im Smoke-Test nie, sonst würde ein automatisierter
752
- // Lauf einen echten Git-Fetch anstoßen.
1089
+ // Lauf einen echten Git-Fetch anstoßen. Der Auslöser (E80) geht mit, damit der Verlauf zeigt, ob
1090
+ // ein Fund vom Start, vom Intervall oder einem Klick kam.
753
1091
  if (!smoketest) {
754
1092
  const fenster = hauptfenster;
755
1093
  fenster.webContents.once('did-finish-load', () => {
756
- fenster.webContents.send('hmgr:aktualisierenAnstossen');
1094
+ fenster.webContents.send('hmgr:aktualisierenAnstossen', 'start');
757
1095
  });
758
- const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
1096
+ const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen', 'intervall'), SECHS_STUNDEN_MS);
759
1097
  fenster.on('closed', () => clearInterval(intervall));
760
1098
  }
761
1099
  // Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
@@ -777,6 +1115,107 @@ function fensterOeffnen() {
777
1115
  }
778
1116
  void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
779
1117
  }
1118
+ const { autoUpdater } = electronUpdater;
1119
+ /** Die Listener hängen am Modul, nicht am Aufruf: `updatePruefen` läuft beim Start und je Prüfen-Klick. */
1120
+ let updaterVerdrahtet = false;
1121
+ /**
1122
+ * Autoupdate der Windows-GUI (E73). Der Feed sind die Gitea-Releases des hmgr-Repos: die
1123
+ * `publish`-Angabe im build-Block der package.json erzeugt beim Bauen die `latest.yml` und legt
1124
+ * `app-update.yml` ins Paket, aus der electron-updater die Feed-URL liest.
1125
+ *
1126
+ * `checkForUpdatesAndNotify()` heißt: im Hintergrund prüfen, ein gefundenes Update herunterladen,
1127
+ * eine Windows-Benachrichtigung zeigen und beim Beenden der App installieren
1128
+ * (`autoInstallOnAppQuit` ist Vorgabe). Das Einspielen bleibt damit der App-Neustart; die GUI zeigt
1129
+ * seit E77 zusätzlich eine Zeile in der Sidebar, sobald das Paket geladen ist (`update-downloaded`).
1130
+ *
1131
+ * Drei Bedingungen, alle nötig:
1132
+ * — `app.isPackaged`: ungepackt gibt es keine `app-update.yml`, die Prüfung stiege ohnehin aus.
1133
+ * — `win32`: macOS lehnt electron-updater unsigniert ab, das wartet auf die Signierung (A4).
1134
+ * — kein Smoke-Test: dieselbe Zurückhaltung wie bei Watcher und Auto-Aktualisieren — ein
1135
+ * automatisierter Lauf greift nie ins Netz.
1136
+ *
1137
+ * Fehler werden nur vermerkt, nie weitergereicht: auf den Team-Rechnern kann das Gitea unerreichbar
1138
+ * sein (VPN, Abschaltung, DNS), und daran darf die App nicht scheitern. Der `error`-Listener ist
1139
+ * dabei nicht optional — electron-updater meldet Fehler als Event, und ein `error`-Event ohne
1140
+ * Zuhörer wirft in Node.
1141
+ */
1142
+ function updatePruefen() {
1143
+ if (smoketest || !app.isPackaged || process.platform !== 'win32')
1144
+ return;
1145
+ try {
1146
+ if (!updaterVerdrahtet) {
1147
+ updaterVerdrahtet = true;
1148
+ autoUpdater.on('error', (fehler) => {
1149
+ console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1150
+ });
1151
+ // E77: das geladene Paket wird sichtbar, seit E80 mit Neustart-Knopf. Zusätzlich ein
1152
+ // Verlaufseintrag „GUI-Update X geladen" (E80), einmal je neuer Version — vor der Meldung an
1153
+ // die Sidebar, damit die Ansicht ihn beim nächsten `ladeVerlauf()` schon findet.
1154
+ autoUpdater.on('update-downloaded', (info) => {
1155
+ const version = info?.version;
1156
+ const guiVersion = typeof version === 'string' ? version : paketVersion();
1157
+ void anwendungsfaelle
1158
+ .hmgrEreignisVermerken({ art: 'gui-update-geladen', version: guiVersion }, letzterAusloeser)
1159
+ .catch((fehler) => {
1160
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1161
+ });
1162
+ selbstUpdateMelden({ art: 'gui', version: guiVersion });
1163
+ });
1164
+ }
1165
+ void autoUpdater.checkForUpdatesAndNotify().catch((fehler) => {
1166
+ console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1167
+ });
1168
+ }
1169
+ catch (fehler) {
1170
+ console.log(`[updater] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1171
+ }
1172
+ }
1173
+ /** Ein Hinweis für die Sidebar (E77). Ist das Fenster schon zu, verfällt er stillschweigend. */
1174
+ function selbstUpdateMelden(nachricht) {
1175
+ if (hauptfenster === null || hauptfenster.isDestroyed())
1176
+ return;
1177
+ hauptfenster.webContents.send('hmgr:selbstUpdate', nachricht);
1178
+ }
1179
+ /**
1180
+ * Die daneben installierte hmgr-CLI gegen npm (E77, F83=O1). Läuft auf allen Plattformen — anders
1181
+ * als die GUI-Prüfung hängt sie an keinem Paket-Format und an keiner Signierung. Gemeldet wird nur,
1182
+ * nie installiert: ein `npm install -g` aus der App scheitert leicht an nvm-Pfaden und Rechten
1183
+ * (F82).
1184
+ *
1185
+ * Still bei allem, was schiefgehen kann: keine CLI im PATH, Fehlercode, Registry unerreichbar,
1186
+ * gleiche oder ältere Version auf npm. Der Login-PATH (F47) muss vorher stehen, sonst findet der
1187
+ * Aufruf die per nvm installierte CLI nicht.
1188
+ */
1189
+ async function cliUpdatePruefen() {
1190
+ await pathBereit;
1191
+ const installiert = await hmgrCliVersion();
1192
+ if (installiert === null)
1193
+ return;
1194
+ const hinweis = await cliUpdateHinweis(installiert, { protokoll: (zeile) => console.log(zeile) });
1195
+ if (hinweis === null)
1196
+ return;
1197
+ // Verlaufseintrag „CLI X verfügbar" (E80), einmal je neuer Version, vor der Sidebar-Meldung.
1198
+ await anwendungsfaelle
1199
+ .hmgrEreignisVermerken({ art: 'cli-verfuegbar', version: hinweis.neu }, letzterAusloeser)
1200
+ .catch((fehler) => {
1201
+ console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1202
+ });
1203
+ selbstUpdateMelden({ art: 'cli', ...hinweis });
1204
+ }
1205
+ /**
1206
+ * Jeder Aktualisieren-Lauf prüft auch hmgr selbst (E80: Start, Intervall, Klick auf den Kringel) —
1207
+ * der Renderer ruft das aus `aktualisierenLauf`, unabhängig vom Auslöser. Beide Wege sind
1208
+ * unabhängig: die GUI-Prüfung gilt nur unter den E73-Bedingungen, die CLI-Prüfung überall. Im
1209
+ * Smoke-Test unterbleiben beide — kein Spawn, kein Netz.
1210
+ */
1211
+ function selbstUpdatePruefen() {
1212
+ if (smoketest)
1213
+ return;
1214
+ updatePruefen();
1215
+ void cliUpdatePruefen().catch((fehler) => {
1216
+ console.log(`[cli-update] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1217
+ });
1218
+ }
780
1219
  /**
781
1220
  * Holt das vorhandene Fenster nach vorn — die Antwort der laufenden Instanz auf einen zweiten
782
1221
  * Start (E67). Minimiert kommt es erst wieder hoch, dann bekommt es den Fokus.
@@ -811,6 +1250,8 @@ if (einzelinstanz) {
811
1250
  Menu.setApplicationMenu(null);
812
1251
  }
813
1252
  fensterOeffnen();
1253
+ // Seit E80 prüft schon der erste Aktualisieren-Lauf (Renderer, ausgelöst bei `did-finish-load`)
1254
+ // auch hmgr selbst über `selbstUpdatePruefen` — ein eigener Start-Aufruf hier wäre der zweite.
814
1255
  app.on('activate', () => {
815
1256
  if (BrowserWindow.getAllWindows().length === 0)
816
1257
  fensterOeffnen();