hmgr 0.0.6 → 0.0.7

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/cli/main.js CHANGED
@@ -4,8 +4,8 @@ import path from 'node:path';
4
4
  import { realpathSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
- import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
8
- import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
7
+ import { Anwendungsfaelle, } from "../kern/anwendungsfaelle.js";
8
+ import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
9
9
  import { Agents } from "../adapter/agents.js";
10
10
  import { ClaudeCode } from "../adapter/claude-code.js";
11
11
  import { Hermes } from "../adapter/hermes.js";
@@ -13,6 +13,7 @@ import { ComposerZeiger } from "../adapter/composer.js";
13
13
  import { DateisystemZiele } from "../adapter/dateisystem.js";
14
14
  import { GitQuellen } from "../adapter/git.js";
15
15
  import { DateiMcpKatalog } from "../adapter/mcp-katalog.js";
16
+ import { paketVersion } from "../adapter/paket-version.js";
16
17
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
17
18
  import { TerminalRueckfrage } from "../adapter/terminal.js";
18
19
  import { ProzessUmgebung } from "../adapter/umgebung.js";
@@ -59,6 +60,7 @@ const SCHALTER = {
59
60
  unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
60
61
  adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
61
62
  plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
63
+ bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: [] },
62
64
  mcp: { werte: ['harness', 'ort'], flaggen: [] },
63
65
  sync: { werte: [], flaggen: [] },
64
66
  update: { werte: [], flaggen: [] },
@@ -87,6 +89,13 @@ const UNTERKOMMANDOS = {
87
89
  remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
88
90
  list: { woerter: 1, schalter: [] },
89
91
  },
92
+ bundle: {
93
+ add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle'] },
94
+ remove: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
95
+ // sync nimmt einen Bundle-Namen, aber keinen zwingend: ohne Namen gilt der ganze Bestand.
96
+ sync: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
97
+ list: { woerter: 1, schalter: [] },
98
+ },
90
99
  mcp: {
91
100
  add: { woerter: 2, schalter: ['harness', 'ort'] },
92
101
  remove: { woerter: 2, schalter: ['harness', 'ort'] },
@@ -179,6 +188,11 @@ const UEBERNAHME_GRUND = {
179
188
  export async function ausfuehren(argv, umgebung, optionen = {}) {
180
189
  const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
181
190
  const kommando = argv[0];
191
+ // Version vor allem anderen (E72): nur die Nummer, damit ein Skript sie ohne Nacharbeit liest.
192
+ if (kommando === '--version') {
193
+ console.log(paketVersion());
194
+ return 0;
195
+ }
182
196
  // Hilfe zuerst: --help ist überall eine Flagge, damit kein Kommando sie als Wert liest.
183
197
  if (kommando === undefined) {
184
198
  console.log(HILFE);
@@ -377,6 +391,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
377
391
  if (kommando === 'unlink') {
378
392
  const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
379
393
  console.log(`Link ${skill} entfernt.`);
394
+ // Zerbrochen (E69): die übrigen Links des Bundles bleiben als Einzel-Installationen liegen.
395
+ for (const bundle of ergebnis.zerbrochen)
396
+ console.log(`Bundle „${bundle}" ist damit zerbrochen, seine übrigen Links bleiben als einzelne Links liegen.`);
380
397
  if (ergebnis.hinweis !== undefined)
381
398
  console.log(ergebnis.hinweis);
382
399
  return 0;
@@ -490,6 +507,89 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
490
507
  console.log(` ${zeile}`);
491
508
  return 0;
492
509
  }
510
+ if (kommando === 'bundle') {
511
+ const schalter = schalterLesen(argv.slice(1), 'bundle');
512
+ unterkommandoPruefen('bundle', schalter);
513
+ const { rest, werte } = schalter;
514
+ const unterkommando = rest[0];
515
+ if (unterkommando === 'list') {
516
+ const katalog = await anwendungsfaelle.bundleKatalog();
517
+ const leer = katalog.every((k) => k.bundles.length === 0 && k.kaputte.length === 0 && k.fehler === undefined);
518
+ if (leer)
519
+ console.log('Keine Quelle mit Bundles. hmgr help bundle zeigt das Format von bundles/<name>.yaml.');
520
+ for (const eintrag of katalog) {
521
+ if (eintrag.fehler !== undefined) {
522
+ console.log(`Quelle ${quelleName(eintrag.quelle)} (${quelleAlsText(eintrag.quelle)}) nicht lesbar: ${eintrag.fehler}`);
523
+ continue;
524
+ }
525
+ if (eintrag.bundles.length === 0 && eintrag.kaputte.length === 0)
526
+ continue;
527
+ console.log(`Quelle ${quelleName(eintrag.quelle)} (${quelleAlsText(eintrag.quelle)})`);
528
+ for (const bundle of eintrag.bundles) {
529
+ console.log(` ${bundle.name} ${kappen(bundle.beschreibung)}`);
530
+ for (const mitglied of bundle.mitglieder)
531
+ console.log(` ${mitglied.pfad} aus ${quelleAlsText(mitglied.quelle)}`);
532
+ }
533
+ // Eine kaputte Definition verdeckt die übrigen nicht, sie wird eigens genannt (E69).
534
+ for (const kaputt of eintrag.kaputte)
535
+ console.log(` Fehler ${kaputt.datei}: ${kaputt.fehler}`);
536
+ }
537
+ return 0;
538
+ }
539
+ if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'sync')
540
+ throw new Error(`${unterkommando === undefined ? 'bundle braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für bundle`}: bundle add <name>, bundle remove <name>, bundle sync [name] oder bundle list. hmgr help bundle zeigt Beispiele.`);
541
+ const name = rest[1];
542
+ if (name === undefined && unterkommando !== 'sync')
543
+ throw new Error(`bundle ${unterkommando} braucht einen Bundle-Namen. hmgr bundle list zeigt die Bundles der bekannten Quellen.`);
544
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
545
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
546
+ // Wie bei link: Verzeichnis merken und binden, damit status es wiederfindet (E21).
547
+ const composer = werte.get('composer');
548
+ if (composer === undefined)
549
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
550
+ else
551
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
552
+ }
553
+ if (unterkommando === 'add') {
554
+ const quelle = werte.get('quelle');
555
+ const ergebnis = await anwendungsfaelle.bundleInstallieren({
556
+ bundle: name,
557
+ ziel,
558
+ ...(quelle === undefined ? {} : { quelle }),
559
+ });
560
+ console.log(`Bundle ${ergebnis.bundle.name} in ${ziel.harness} ${beschreibeOrt(ziel.ort)} installiert (${quelleAlsText(ergebnis.bundle.quelle)}).`);
561
+ for (const neu of ergebnis.neueQuellen)
562
+ console.log(` neue Quelle ${quelleAlsText(neu)} als „${quelleName(neu)}"`);
563
+ if (ergebnis.gelegt.length > 0)
564
+ console.log(` gelegt ${ergebnis.gelegt.join(', ')}`);
565
+ // Übernommen (F67): schon vorhandene Links derselben Quelle gehören jetzt zum Bundle.
566
+ if (ergebnis.uebernommen.length > 0)
567
+ console.log(` übernommen ${ergebnis.uebernommen.join(', ')}`);
568
+ if (ergebnis.hinweis !== undefined)
569
+ for (const zeile of ergebnis.hinweis.split('\n'))
570
+ console.log(` ${zeile}`);
571
+ return 0;
572
+ }
573
+ if (unterkommando === 'remove') {
574
+ const ergebnis = await anwendungsfaelle.bundleEntfernen({ bundle: name, ziel });
575
+ console.log(`Bundle ${ergebnis.bundle} aus ${ziel.harness} ${beschreibeOrt(ziel.ort)} entfernt.`);
576
+ console.log(ergebnis.entfernt.length === 0
577
+ ? ' Kein Link mehr da, nur der Bundle-Verweis ist weg.'
578
+ : ` entfernt ${ergebnis.entfernt.join(', ')}`);
579
+ return 0;
580
+ }
581
+ // sync: ohne Ziel-Schalter der ganze Bestand, mit --ort/--harness nur dieses Ziel.
582
+ const nurZiel = werte.has('harness') || werte.has('ort');
583
+ const eintraege = await anwendungsfaelle.bundleAbgleich({
584
+ ...(name === undefined ? {} : { bundle: name }),
585
+ ...(nurZiel ? { ziel } : {}),
586
+ });
587
+ if (eintraege.length === 0)
588
+ console.log('Kein Bundle installiert, nichts abzugleichen.');
589
+ for (const zeile of bundleBericht(eintraege))
590
+ console.log(zeile);
591
+ return 0;
592
+ }
493
593
  if (kommando === 'mcp') {
494
594
  const schalter = schalterLesen(argv.slice(1), 'mcp');
495
595
  unterkommandoPruefen('mcp', schalter);
@@ -568,13 +668,18 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
568
668
  }
569
669
  if (kommando === 'update') {
570
670
  restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
571
- const { checkouts: liste, pluginHarnesses, aufraeumen } = await anwendungsfaelle.aktualisieren();
671
+ const { checkouts: liste, pluginHarnesses, bundles, aufraeumen } = await anwendungsfaelle.aktualisieren();
572
672
  if (liste.length === 0)
573
673
  console.log('Keine Checkouts.');
574
- for (const { checkout, aktualisierung } of liste) {
674
+ for (const { checkout, aktualisierung, geaenderteSkills } of liste) {
575
675
  const kopf = `${quelleAlsText(checkout.quelle)} @ ${checkout.ref}`;
576
- if (aktualisierung.ergebnis === 'aktualisiert')
676
+ if (aktualisierung.ergebnis === 'aktualisiert') {
577
677
  console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
678
+ // Auf Skill-Ebene berichten (E70): sonst steht da nur, dass die Quelle neue Commits hat.
679
+ // Ohne Treffer bleibt die Zeile weg — der Commit hat dann nichts in einem Skill geändert.
680
+ if (geaenderteSkills.length > 0)
681
+ console.log(` ${zahlwort(geaenderteSkills.length, 'Skill', 'Skills')} geändert: ${geaenderteSkills.join(', ')}`);
682
+ }
578
683
  else if (aktualisierung.ergebnis === 'unveraendert')
579
684
  console.log(` unverändert ${kopf}`);
580
685
  else if (aktualisierung.ergebnis === 'uebersprungen')
@@ -584,6 +689,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
584
689
  }
585
690
  for (const name of pluginHarnesses)
586
691
  console.log(` Plugins ${name} aktualisiert (E14)`);
692
+ // Bundles im selben Lauf (E69, F64=O1): eine geänderte Definition wird hier nachgezogen.
693
+ for (const zeile of bundleBericht(bundles))
694
+ console.log(zeile);
587
695
  // Zum Schluss die verwaisten Checkouts: was gelöscht wurde und was liegen bleibt.
588
696
  for (const verwaist of aufraeumen.verwaiste) {
589
697
  const kopf = `${quelleAlsText(verwaist.checkout.quelle)} @ ${verwaist.checkout.ref}`;
@@ -618,6 +726,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
618
726
  console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.link.name))}`);
619
727
  }
620
728
  }
729
+ // Bundles des Ziels (E69), unter den Einträgen: welche Links als Einheit zusammengehören.
730
+ for (const bundle of zielZustand.bundles)
731
+ console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundle.skills.join(', ')}`);
621
732
  if (zielZustand.plugins !== undefined)
622
733
  console.log(` Plugins: ${zielZustand.plugins.length === 0 ? 'keine' : zielZustand.plugins.join(', ')}`);
623
734
  }
@@ -738,6 +849,31 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
738
849
  }
739
850
  throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
740
851
  }
852
+ /**
853
+ * Bericht über den Bundle-Abgleich (E69), gleich für `hmgr bundle sync` und `hmgr update`: je
854
+ * Bundle eine Zeile mit seinem Ausgang, darunter, was sich geändert hat. „fehlgeschlagen" heißt
855
+ * genau das — der Zusatz sagt, dass das Bundle im Ziel auf dem alten Stand bleibt.
856
+ */
857
+ function bundleBericht(eintraege) {
858
+ const zeilen = [];
859
+ for (const eintrag of eintraege) {
860
+ const kopf = `Bundle ${eintrag.bundle} ${eintrag.ziel.harness} ${beschreibeOrt(eintrag.ziel.ort)}`;
861
+ if (eintrag.ergebnis === 'fehlgeschlagen') {
862
+ zeilen.push(` fehlgeschlagen ${kopf} bleibt auf dem alten Stand`);
863
+ for (const zeile of (eintrag.fehler ?? '').split('\n'))
864
+ zeilen.push(` ${zeile}`);
865
+ continue;
866
+ }
867
+ zeilen.push(` ${eintrag.ergebnis === 'aktualisiert' ? 'aktualisiert ' : 'unverändert '} ${kopf}`);
868
+ for (const neu of eintrag.neueQuellen)
869
+ zeilen.push(` neue Quelle ${quelleAlsText(neu)}`);
870
+ if (eintrag.gelegt.length > 0)
871
+ zeilen.push(` gelegt ${eintrag.gelegt.join(', ')}`);
872
+ if (eintrag.entfernt.length > 0)
873
+ zeilen.push(` entfernt ${eintrag.entfernt.join(', ')}`);
874
+ }
875
+ return zeilen;
876
+ }
741
877
  /** Grund eines Links als Suffix (E37). „manuell" bleibt stumm, damit die Ausgabe ruhig bleibt. */
742
878
  function beschreibeGrund(grund) {
743
879
  if (grund === undefined || grund.art === 'manuell')
@@ -774,6 +910,12 @@ function istModellFehler(fehler) {
774
910
  fehler instanceof PluginMehrdeutig ||
775
911
  fehler instanceof PluginUnbekannt ||
776
912
  fehler instanceof AbhaengigkeitFehlt ||
913
+ fehler instanceof BundleUnbekannt ||
914
+ fehler instanceof BundleMehrdeutig ||
915
+ fehler instanceof BundleMitgliederFehlen ||
916
+ fehler instanceof BundleNamenskonflikte ||
917
+ fehler instanceof BundleNichtInstalliert ||
918
+ fehler instanceof BundleDateiFehler ||
777
919
  fehler instanceof QuelleUnbekannt ||
778
920
  fehler instanceof QuelleNameBelegt ||
779
921
  fehler instanceof QuelleAufraeumenLokaleAenderungen ||
@@ -808,6 +950,18 @@ function naechsterSchritt(fehler) {
808
950
  return 'hmgr plugin list zeigt die Plugins der bekannten Quellen. Fehlt der Marketplace, erst hmgr source add <url>.';
809
951
  if (fehler instanceof AbhaengigkeitFehlt)
810
952
  return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
953
+ if (fehler instanceof BundleUnbekannt)
954
+ return 'hmgr bundle list zeigt die Bundles der bekannten Quellen. Fehlt die Quelle, erst hmgr source add <url|pfad>.';
955
+ if (fehler instanceof BundleMehrdeutig)
956
+ return 'Quelle eingrenzen: --quelle <teilstring der URL oder des Pfads>. hmgr bundle list zeigt, wo welches Bundle steht.';
957
+ if (fehler instanceof BundleMitgliederFehlen)
958
+ return 'Das Bundle ist nur ganz oder gar nicht zu haben. Wer nur einen Teil braucht, verlinkt die erreichbaren Skills einzeln mit hmgr link.';
959
+ if (fehler instanceof BundleNamenskonflikte)
960
+ return 'Die genannten Namen erst freimachen (hmgr unlink <skill> bzw. hmgr adopt <name>), dann bundle add erneut.';
961
+ if (fehler instanceof BundleNichtInstalliert)
962
+ return 'hmgr status zeigt je Ziel, welche Bundles dort installiert sind.';
963
+ if (fehler instanceof BundleDateiFehler)
964
+ return 'Die Definition liegt in der Quelle und wird dort korrigiert, hmgr schreibt sie nie. hmgr help bundle zeigt das Format.';
811
965
  if (fehler instanceof KeinPassenderRef)
812
966
  return 'Die Tags der Quelle sind frisch geholt, keiner passt. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
813
967
  if (fehler instanceof HarnessNichtVorhanden)
package/dist/gui/main.js CHANGED
@@ -9,6 +9,7 @@ import { stat } from 'node:fs/promises';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { bauen, umgebungLesen } from "../cli/main.js";
11
11
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
12
+ import { paketVersion } from "../adapter/paket-version.js";
12
13
  import { quelleName, quelleSchluessel } from "../kern/modell.js";
13
14
  import { kannProfile } from "../ports/harness.js";
14
15
  import { iconFuerQuelle } from "./icons.js";
@@ -136,6 +137,14 @@ function zielZustandSerialisieren(z) {
136
137
  gruende: Object.fromEntries(z.gruende),
137
138
  ignoriert: z.ignoriert,
138
139
  uebernahme: Object.fromEntries(z.uebernahme),
140
+ // Bundles des Ziels (E69): Adresse und Anzeigename der Quelle kommen mit, damit der Renderer
141
+ // die Gruppe beschriften kann, ohne die Quelle selbst nachzuschlagen.
142
+ bundles: z.bundles.map((b) => ({
143
+ name: b.name,
144
+ skills: b.skills,
145
+ quelleText: quelleAlsText(b.quelle),
146
+ quelleName: quelleName(b.quelle),
147
+ })),
139
148
  istRegistriert: z.istRegistriert,
140
149
  kannSkillOrdner: z.kannSkillOrdner,
141
150
  ...(z.plugins === undefined ? {} : { plugins: z.plugins }),
@@ -181,6 +190,7 @@ function registriereIpc() {
181
190
  ...(skill.modell === undefined ? {} : { modell: skill.modell }),
182
191
  ...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
183
192
  ...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
193
+ ...(skill.geaendert === undefined ? {} : { geaendert: skill.geaendert }),
184
194
  quelleText: quelleAlsText(eintrag.quelle),
185
195
  quelleName: quelleName(eintrag.quelle),
186
196
  quelleSchluessel: quelleSchluessel(eintrag.quelle),
@@ -189,6 +199,45 @@ function registriereIpc() {
189
199
  }
190
200
  return ergebnis;
191
201
  }));
202
+ // Bundles aller bekannten Quellen (E69): flach wie der Skill-Katalog, je Bundle seine Quelle und
203
+ // die Zahl seiner Mitglieder. Kaputte Definitionen kommen als eigene Liste mit, damit die GUI sie
204
+ // nennen kann, statt sie zu verschlucken.
205
+ ipcMain.handle('hmgr:bundles', async () => auffangen(async () => {
206
+ const bundles = [];
207
+ const kaputte = [];
208
+ for (const eintrag of await anwendungsfaelle.bundleKatalog()) {
209
+ for (const bundle of eintrag.bundles)
210
+ bundles.push({
211
+ name: bundle.name,
212
+ beschreibung: bundle.beschreibung,
213
+ mitglieder: bundle.mitglieder.length,
214
+ // Anzeige im Dialog: aus welchen Quellen das Bundle Skills holt, ohne Dubletten.
215
+ quellen: [...new Set(bundle.mitglieder.map((m) => quelleAlsText(m.quelle)))],
216
+ quelleText: quelleAlsText(bundle.quelle),
217
+ quelleName: quelleName(bundle.quelle),
218
+ });
219
+ for (const kaputt of eintrag.kaputte)
220
+ kaputte.push({ quelleName: quelleName(eintrag.quelle), datei: kaputt.datei, fehler: kaputt.fehler });
221
+ }
222
+ return { bundles, kaputte };
223
+ }));
224
+ // Installieren und Entfernen wirken auf das Bundle als Ganzes (E69). Der Zeiger ist nicht im
225
+ // Spiel, die Zugehörigkeit steht im Zustand — anders als bei linkLegen braucht es kein `lokal`.
226
+ ipcMain.handle('hmgr:bundleInstallieren', async (_event, args) => auffangen(async () => {
227
+ const ergebnis = await anwendungsfaelle.bundleInstallieren({
228
+ bundle: args.bundle,
229
+ ziel: zielAusIpc(args.harness, args.ort),
230
+ ...(args.quelle === undefined ? {} : { quelle: args.quelle }),
231
+ });
232
+ return {
233
+ name: ergebnis.bundle.name,
234
+ gelegt: ergebnis.gelegt,
235
+ uebernommen: ergebnis.uebernommen,
236
+ neueQuellen: ergebnis.neueQuellen.map((q) => quelleName(q)),
237
+ ...(ergebnis.hinweis === undefined ? {} : { hinweis: ergebnis.hinweis }),
238
+ };
239
+ }));
240
+ ipcMain.handle('hmgr:bundleEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.bundleEntfernen({ bundle: args.bundle, ziel: zielAusIpc(args.harness, args.ort) })));
192
241
  // Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
193
242
  ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
194
243
  art: quelle.art,
@@ -386,6 +435,11 @@ function fensterOeffnen() {
386
435
  minWidth: 860,
387
436
  minHeight: 560,
388
437
  backgroundColor: '#E3E7E0', // --platte, verhindert einen weißen Blitz beim Start
438
+ // E72: der Fenstertitel nennt die Version, damit im Feld erkennbar ist, welcher Stand läuft.
439
+ // Nicht `app.getVersion()`: das liefert nur im gepackten Bundle die Paketversion, beim Start
440
+ // über `electron dist/gui/main.js` (npm run gui, Smoke-Test) dagegen die Electron-Version —
441
+ // dort liegt neben dem Hauptskript keine package.json (gemessen: 44.0.0 statt 0.0.6).
442
+ title: `hmgr ${paketVersion()}`,
389
443
  // Im Smoke-Test bleibt das Fenster unsichtbar: der Renderer lädt trotzdem vollständig, der
390
444
  // DOM-Auszug kommt per executeJavaScript. So klaut ein automatisierter Lauf niemandem den
391
445
  // Eingabefokus und öffnet kein sichtbares Fenster.
@@ -400,6 +454,9 @@ function fensterOeffnen() {
400
454
  hauptfenster.on('closed', () => {
401
455
  hauptfenster = null;
402
456
  });
457
+ // Der <title> der index.html setzt den Fenstertitel beim Laden sonst zurück und überschriebe die
458
+ // Option oben (E72). Der DOM-Titel bleibt dabei, wie er ist, nur das Fenster folgt ihm nicht mehr.
459
+ hauptfenster.on('page-title-updated', (ereignis) => ereignis.preventDefault());
403
460
  // Renderer-Konsole im Main-Log, für Fehlersuche und den Smoke-Test.
404
461
  hauptfenster.webContents.on('console-message', (event) => {
405
462
  console.log(`[renderer] ${event.message}`);
@@ -507,6 +564,9 @@ function fensterOeffnen() {
507
564
  // Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
508
565
  document.querySelector('[data-ansicht="katalog"]')?.click();
509
566
  var katalogBeschreibung = document.querySelector('#katalog .zeile .beschreibung')?.textContent ?? null;
567
+ // E70/F70=O1: die Herkunftszeile trägt hinter dem Ref das Änderungsdatum des Skills.
568
+ var katalogHerkunft = document.querySelector('#katalog .zeile .herkunft')?.textContent
569
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
510
570
  // Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
511
571
  // einem Klick auf den Aufklapp-Knopf.
512
572
  var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
@@ -524,12 +584,34 @@ function fensterOeffnen() {
524
584
  var dskKnopf = Array.from(document.querySelectorAll('button'))
525
585
  .find((b) => b.textContent === 'Skills hinzufügen');
526
586
  var dskMarken = null;
587
+ // Bundle-Sektion im selben Dialog (E69, F68): sie steht dort, wo die Entscheidung
588
+ // fällt, was ins Ziel kommt, und zeigt je Bundle Beschreibung und Herkunft.
589
+ var dskBundleBoxSichtbar = false;
590
+ var dskBundleText = null;
527
591
  if (dskKnopf) {
528
592
  dskKnopf.click();
529
593
  dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
530
594
  ?.replace(/\\s+/g, ' ').trim() ?? null;
595
+ dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
596
+ dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
597
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
531
598
  document.getElementById('d-skills')?.close();
532
599
  }
600
+ // Bundle-Gruppe an der Ziel-Karte (E69): Kopf mit Name, Quelle und Entfernen, darunter
601
+ // die Zeilen der Mitglieder — sie stehen in der Gruppe, nicht in der allgemeinen Liste.
602
+ var bundleGruppeText = document.querySelector('#zielliste .bundlegruppe .bundlekopf')?.textContent
603
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
604
+ var bundleGruppeZeilen = Array.from(document.querySelectorAll('#zielliste .bundlegruppe .linkliste .name'))
605
+ .map((n) => n.textContent.trim()).join(',');
606
+ // Entfernen an der Einzelzeile innerhalb der Gruppe sagt, dass das Bundle zerbricht.
607
+ var bundleZerbrichtText = null;
608
+ try {
609
+ document.querySelector('#zielliste .bundlegruppe .linkliste .knopf')?.click();
610
+ bundleZerbrichtText = document.getElementById('dx-text')?.textContent ?? null;
611
+ document.querySelector('#d-entfernen button[value="nein"]')?.click();
612
+ } catch (fehler) {
613
+ bundleZerbrichtText = String(fehler);
614
+ }
533
615
  // MCP-Ansicht (E61-Folgerunde, F46: abgespeckt zum reinen Katalog): Navigationsreihenfolge
534
616
  // Ziele, MCP, Plugins, Skills, Karten mit Name/Beschreibung/Homepage-Link, F39-Warnzeile,
535
617
  // Einhängen-Dialog öffnen/abbrechen ohne IPC-Schreibaktion. Orte-Liste und Fremdkasten
@@ -640,6 +722,14 @@ function fensterOeffnen() {
640
722
  ?.replace(/\\s+/g, ' ').trim() ?? null;
641
723
  var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
642
724
  var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
725
+ // Kartenkopf-Zahl (E71): der Kopf zählt Skills, gebündelt wie der Körper zeilt. An der
726
+ // Verzeichnis-Karte kunde-x hängen dieselben zwei Skills an zwei Harnesses, eine Summe
727
+ // über die Abschnitte ergäbe hier 4.
728
+ var kopfZahlVon = function (karte) {
729
+ return karte?.querySelector('.zielkopf .zahl')?.textContent?.trim() ?? null;
730
+ };
731
+ var kundeXKopfZahl = kopfZahlVon(kundeXKarte);
732
+ var hermesKopfZahl = kopfZahlVon(hermesKarte);
643
733
  var agentsKarte = karteMitKopftext('Agents · Home');
644
734
  // Ohne diese Zusicherung wären die beiden Abwesenheits-Prüfungen darunter leer erfüllt,
645
735
  // sobald die Karte selbst aus der Liste fiele: „kein Abschnitt" hieße dann nur „keine Karte".
@@ -678,12 +768,18 @@ function fensterOeffnen() {
678
768
  zielManuellVorhanden,
679
769
  zahnradVorhanden,
680
770
  katalogBeschreibung,
771
+ katalogHerkunft,
681
772
  katalogMarken,
682
773
  katalogDetailsZu,
683
774
  katalogAufgeklappt,
684
775
  katalogDetails,
685
776
  katalogMarkenWarnend,
686
777
  dskMarken,
778
+ dskBundleBoxSichtbar,
779
+ dskBundleText,
780
+ bundleGruppeText,
781
+ bundleGruppeZeilen,
782
+ bundleZerbrichtText,
687
783
  interGeladen: document.fonts.check('400 16px Inter'),
688
784
  monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
689
785
  quelleneuVorhanden,
@@ -733,6 +829,8 @@ function fensterOeffnen() {
733
829
  kundeXMcpText,
734
830
  kundeXMcpZeilenKnopfZahl,
735
831
  kundeXMcpKnopfVorhanden,
832
+ kundeXKopfZahl,
833
+ hermesKopfZahl,
736
834
  agentsKarteVorhanden,
737
835
  agentsMcpAbschnittVorhanden,
738
836
  agentsMcpKnopfVorhanden,
@@ -743,6 +841,9 @@ function fensterOeffnen() {
743
841
  });
744
842
  })()`)
745
843
  .then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
844
+ // E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
845
+ // index.html), er lässt sich nur hier im Main-Prozess ablesen.
846
+ .then(() => console.log(`[smoke] Fenster: ${JSON.stringify({ fenstertitel: fenster.getTitle() })}`))
746
847
  .finally(() => app.quit());
747
848
  }, 3000);
748
849
  });
@@ -11,6 +11,10 @@ contextBridge.exposeInMainWorld('hmgr', {
11
11
  marketplaces: () => ipcRenderer.invoke('hmgr:marketplaces'),
12
12
  skills: () => ipcRenderer.invoke('hmgr:skills'),
13
13
  quellen: () => ipcRenderer.invoke('hmgr:quellen'),
14
+ /** Bundles aller bekannten Quellen (E69), entdeckt beim Quellen-Scan: kein eigener Katalog. */
15
+ bundles: () => ipcRenderer.invoke('hmgr:bundles'),
16
+ bundleInstallieren: (args) => ipcRenderer.invoke('hmgr:bundleInstallieren', args),
17
+ bundleEntfernen: (args) => ipcRenderer.invoke('hmgr:bundleEntfernen', args),
14
18
  /** Icon einer Quelle als data-URL, oder null (E60). */
15
19
  icon: (text) => ipcRenderer.invoke('hmgr:icon', text),
16
20
  /** Öffnet eine Quelle: Repo-Seite im Browser oder Ordner im Dateimanager (E60). */
@@ -216,7 +216,18 @@
216
216
  <svg viewBox="0 0 16 16" stroke-width="1.6" aria-hidden="true"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14.5 14.5"/></svg>
217
217
  <input type="search" id="dsk-suche" placeholder="Skill suchen" aria-label="Skills durchsuchen">
218
218
  </div>
219
- <div class="wahl" id="dsk-liste"></div>
219
+ <!-- Bundle-Sektion (E69, F68): kein eigener Navigationspunkt, Bundles stehen hier, wo die
220
+ Entscheidung fällt, was ins Ziel kommt. Leer bleibt der Kasten weg. -->
221
+ <div class="dskgruppe" id="dsk-bundlebox" hidden>
222
+ <span class="etikett">Bundles</span>
223
+ <p class="ohne dskgruppentext">Ein Bundle bringt eine ganze Zusammenstellung auf einmal, auch über Quellen hinweg. Fehlende Quellen legt hmgr dabei an.</p>
224
+ <div class="wahl" id="dsk-bundles"></div>
225
+ <p class="ohne dskgruppenfehler" id="dsk-bundlefehler" hidden></p>
226
+ </div>
227
+ <div class="dskgruppe">
228
+ <span class="etikett" id="dsk-skilletikett">Skills</span>
229
+ <div class="wahl" id="dsk-liste"></div>
230
+ </div>
220
231
  </div>
221
232
  <div class="dfuss">
222
233
  <span class="links mono" id="dsk-bilanz">0 ausgewählt</span>