hmgr 0.0.45 → 0.0.47

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,7 +4,7 @@ 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 { checkoutGruppen, quellenAnzeigeName, sammelZeile, verlaufZeitpunkt } from "./verlauf-anzeige.js";
7
+ import { checkoutGruppen, quellenAnzeigeName, sammelZeile, sopsGrundText, sopsZeile, verlaufZeitpunkt, } from "./verlauf-anzeige.js";
8
8
  import { Anwendungsfaelle, pluginWeltText, } from "../kern/anwendungsfaelle.js";
9
9
  import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, ClaudeCliNichtErreichbar, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
10
10
  import { Agents } from "../adapter/agents.js";
@@ -19,6 +19,7 @@ import { DateiMcpKatalog } from "../adapter/mcp-katalog.js";
19
19
  import { cliUpdateHinweis } from "../adapter/npm-registry.js";
20
20
  import { paketVersion } from "../adapter/paket-version.js";
21
21
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
22
+ import { standardSops } from "../adapter/sops.js";
22
23
  import { TerminalRueckfrage } from "../adapter/terminal.js";
23
24
  import { ProzessUmgebung } from "../adapter/umgebung.js";
24
25
  import { signalSchreiben } from "../adapter/gui-signal.js";
@@ -92,7 +93,7 @@ export function bauen(umgebung, optionen = {}) {
92
93
  });
93
94
  const ports = {
94
95
  harnesses: [claude, new Hermes(heime, zugriffFuerOrt), new Agents(heime, zugriffFuerOrt)],
95
- quellen: new GitQuellen(wurzel),
96
+ quellen: new GitQuellen(wurzel, { sops: optionen.sops ?? standardSops(umgebung.home) }),
96
97
  ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome, (ziel) => zugriffFuerOrt(ziel.ort)),
97
98
  zeiger: new ComposerZeiger(),
98
99
  zustand,
@@ -125,7 +126,7 @@ const SCHALTER = {
125
126
  unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
126
127
  adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
127
128
  plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
128
- bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: [] },
129
+ bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['complete', 'vervollstaendigen'] },
129
130
  mcp: { werte: ['harness', 'ort'], flaggen: [] },
130
131
  sync: { werte: [], flaggen: [] },
131
132
  update: { werte: ['quelle'], flaggen: [] },
@@ -160,7 +161,8 @@ const UNTERKOMMANDOS = {
160
161
  add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle'] },
161
162
  remove: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
162
163
  // sync nimmt einen Bundle-Namen, aber keinen zwingend: ohne Namen gilt der ganze Bestand.
163
- sync: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
164
+ // --complete/--vervollstaendigen holt herausgenommene Mitglieder zurück (E114 L4).
165
+ sync: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'complete', 'vervollstaendigen'] },
164
166
  list: { woerter: 1, schalter: [] },
165
167
  },
166
168
  mcp: {
@@ -491,9 +493,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
491
493
  if (kommando === 'unlink') {
492
494
  const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
493
495
  console.log(`Link ${skill} entfernt.`);
494
- // Zerbrochen (E69): die übrigen Links des Bundles bleiben als Einzel-Installationen liegen.
495
- for (const bundle of ergebnis.zerbrochen)
496
- console.log(`Bundle „${bundle}" ist damit zerbrochen, seine übrigen Links bleiben als einzelne Links liegen.`);
496
+ // Herausgenommen (E114 L2): das Bundle bleibt, der Abgleich legt den Skill nicht wieder an.
497
+ for (const bundle of ergebnis.herausgenommenAus)
498
+ console.log(`„${skill}" ist aus dem Bundle „${bundle}" herausgenommen, das Bundle bleibt. ` +
499
+ `Zurück mit hmgr link ${skill} oder hmgr bundle sync ${bundle} --complete.`);
497
500
  if (ergebnis.hinweis !== undefined)
498
501
  console.log(ergebnis.hinweis);
499
502
  return 0;
@@ -612,10 +615,15 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
612
615
  if (kommando === 'bundle') {
613
616
  const schalter = schalterLesen(argv.slice(1), 'bundle');
614
617
  unterkommandoPruefen('bundle', schalter);
615
- const { rest, werte } = schalter;
618
+ const { rest, werte, flaggen } = schalter;
616
619
  const unterkommando = rest[0];
617
620
  if (unterkommando === 'list') {
618
621
  const katalog = await anwendungsfaelle.bundleKatalog();
622
+ // Je Bundle die Ziele, in denen es liegt, mit seiner Lage (E114 L6). Nur nötig, wenn
623
+ // überhaupt ein Bundle installiert ist; der Ist-Zustand kostet sonst einen Scan aller Ziele.
624
+ const installiert = (await ports.zustand.lesen()).bundleInstallationen.length > 0
625
+ ? (await anwendungsfaelle.istZustand({ plugins: false })).flatMap((z) => z.bundles.map((b) => ({ ziel: z.ziel, bundle: b })))
626
+ : [];
619
627
  const leer = katalog.every((k) => k.bundles.length === 0 && k.kaputte.length === 0 && k.fehler === undefined);
620
628
  if (leer)
621
629
  console.log('Keine Quelle mit Bundles. hmgr help bundle zeigt das Format von bundles/<name>.yaml.');
@@ -631,6 +639,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
631
639
  console.log(` ${bundle.name} ${kappen(bundle.beschreibung)}`);
632
640
  for (const mitglied of bundle.mitglieder)
633
641
  console.log(` ${mitglied.pfad} aus ${quelleAlsText(mitglied.quelle)}`);
642
+ for (const { ziel, bundle: lage } of installiert)
643
+ if (lage.name === bundle.name && quelleSchluessel(lage.quelle) === quelleSchluessel(bundle.quelle))
644
+ console.log(` installiert ${ziel.harness} ${beschreibeOrt(ziel.ort)} ${bundleLage(lage)}`);
634
645
  }
635
646
  // Eine kaputte Definition verdeckt die übrigen nicht, sie wird eigens genannt (E69).
636
647
  for (const kaputt of eintrag.kaputte)
@@ -683,9 +694,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
683
694
  }
684
695
  // sync: ohne Ziel-Schalter der ganze Bestand, mit --ort/--harness nur dieses Ziel.
685
696
  const nurZiel = werte.has('harness') || werte.has('ort');
697
+ const vervollstaendigen = flaggen.has('complete') || flaggen.has('vervollstaendigen');
686
698
  const eintraege = await anwendungsfaelle.bundleAbgleich({
687
699
  ...(name === undefined ? {} : { bundle: name }),
688
700
  ...(nurZiel ? { ziel } : {}),
701
+ ...(vervollstaendigen ? { aufnehmen: 'alle' } : {}),
689
702
  });
690
703
  if (eintraege.length === 0)
691
704
  console.log('Kein Bundle installiert, nichts abzugleichen.');
@@ -843,12 +856,17 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
843
856
  else
844
857
  console.log(` liegen lassen Arbeitsverzeichnis ${pfad} (nicht mehr auf der Platte, ohne Bestätigung nicht vergessen)`);
845
858
  }
859
+ // SOPS-Dateien, die verschlüsselt im Checkout lagen (E115).
860
+ const sopsQuellen = doctor.sops.length === 0 ? [] : await anwendungsfaelle.quellen();
861
+ for (const zeile of sopsBerichtZeilen(doctor.sops, sopsQuellen))
862
+ console.log(zeile);
846
863
  // Die Sammelzeile steht ganz am Ende des Berichts (E109); „Außerdem" nur, wenn sonst etwas im Lauf stand.
847
864
  const updateSammel = sammelZeile(updateOhne, !updateAndere &&
848
865
  pluginFehler.length === 0 &&
849
866
  bundleBericht(bundles).length === 0 &&
850
867
  aufraeumen.verwaiste.length === 0 &&
851
- doctor.fehlendeArbeitsverzeichnisse.length === 0);
868
+ doctor.fehlendeArbeitsverzeichnisse.length === 0 &&
869
+ doctor.sops.length === 0);
852
870
  if (updateSammel !== undefined)
853
871
  console.log(updateSammel);
854
872
  // Zuletzt hmgr selbst (E77): wer schon aktualisiert, will auch wissen, dass sein hmgr alt ist.
@@ -888,13 +906,22 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
888
906
  }
889
907
  // Bundles des Ziels (E69), unter den Einträgen: welche Links als Einheit zusammengehören.
890
908
  for (const bundle of zielZustand.bundles)
891
- console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundle.skills.join(', ')}`);
909
+ console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundleLage(bundle)}`);
892
910
  if (zielZustand.plugins !== undefined)
893
911
  console.log(` Plugins: ${zielZustand.plugins.length === 0 ? 'keine' : zielZustand.plugins.join(', ')}`);
894
912
  }
895
913
  const fehlend = await anwendungsfaelle.nichtVorhandeneHarnesses();
896
914
  if (fehlend.length > 0)
897
915
  console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
916
+ // SOPS-Dateien, die noch verschlüsselt im Checkout liegen (E115); ohne Fund keine Zeile.
917
+ const verschluesselt = await anwendungsfaelle.sopsVerschluesselteDateien();
918
+ if (verschluesselt.length > 0) {
919
+ const quellen = await anwendungsfaelle.quellen();
920
+ console.log('Noch verschlüsselt im Checkout:');
921
+ for (const { checkout, datei } of verschluesselt)
922
+ console.log(` ${datei} aus ${quellenAnzeigeName({ quelle: quelleAlsText(checkout.quelle), ref: checkout.ref }, quellen)} (${path.join(checkout.pfad, datei)})`);
923
+ console.log('hmgr doctor versucht, sie zu entschlüsseln, und nennt den Grund, wenn es nicht klappt.');
924
+ }
898
925
  return 0;
899
926
  }
900
927
  if (kommando === 'log') {
@@ -924,7 +951,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
924
951
  if (ereignis.art === 'checkout')
925
952
  continue;
926
953
  andere = true;
927
- console.log(` ${verlaufEreignisZeile(ereignis)}`);
954
+ console.log(` ${verlaufEreignisZeile(ereignis, quellenListe)}`);
928
955
  // Unter einem Versionssprung die Commits der neuen Version(en), eingerückt (E108).
929
956
  if (ereignis.art === 'hmgr-installiert') {
930
957
  const aenderungen = await anwendungsfaelle.hmgrAenderungen(ereignis.vorher, ereignis.version);
@@ -940,7 +967,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
940
967
  }
941
968
  if (kommando === 'doctor') {
942
969
  restPruefen(schalterLesen(argv.slice(1), 'doctor').rest, 0, 'doctor');
943
- const { fehlendeArbeitsverzeichnisse, vergessen } = await anwendungsfaelle.doctor();
970
+ const { fehlendeArbeitsverzeichnisse, vergessen, sops } = await anwendungsfaelle.doctor();
944
971
  if (fehlendeArbeitsverzeichnisse.length === 0)
945
972
  console.log('Keine fehlenden Arbeitsverzeichnisse.');
946
973
  for (const pfad of fehlendeArbeitsverzeichnisse) {
@@ -949,6 +976,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
949
976
  else
950
977
  console.log(` liegen lassen ${pfad} (ohne Bestätigung nicht vergessen)`);
951
978
  }
979
+ // SOPS-Dateien (E115): nur bei einem Fund eine Zeile, wer kein SOPS nutzt, sieht nichts davon.
980
+ const sopsQuellen = sops.length === 0 ? [] : await anwendungsfaelle.quellen();
981
+ for (const zeile of sopsBerichtZeilen(sops, sopsQuellen))
982
+ console.log(zeile);
952
983
  return 0;
953
984
  }
954
985
  if (kommando === 'harness') {
@@ -1134,6 +1165,16 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
1134
1165
  }
1135
1166
  throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
1136
1167
  }
1168
+ /**
1169
+ * Lage eines installierten Bundles in einem Ziel (E114 L6), für `status` und `bundle list`:
1170
+ * verlinkt von gesamt, die verlinkten Namen und, wenn etwas fehlt, was.
1171
+ */
1172
+ function bundleLage(bundle) {
1173
+ const gesamt = bundle.skills.length + bundle.nichtVerlinkt.length;
1174
+ const verlinkt = bundle.skills.length === 0 ? '' : ` ${bundle.skills.join(', ')}`;
1175
+ const fehlt = bundle.nichtVerlinkt.length === 0 ? '' : ` fehlt: ${bundle.nichtVerlinkt.join(', ')}`;
1176
+ return `${bundle.skills.length}/${gesamt}${verlinkt}${fehlt}`;
1177
+ }
1137
1178
  /**
1138
1179
  * Bericht über den Bundle-Abgleich (E69), gleich für `hmgr bundle sync` und `hmgr update`: je
1139
1180
  * Bundle eine Zeile mit seinem Ausgang, darunter, was sich geändert hat. „fehlgeschlagen" heißt
@@ -1163,8 +1204,21 @@ function bundleBericht(eintraege) {
1163
1204
  function aenderungsZeilen(aenderungen) {
1164
1205
  return aenderungen.flatMap((a) => [` ${a.version}`, ...a.commits.map((c) => ` ${c}`)]);
1165
1206
  }
1207
+ /** Bericht zu SOPS-Dateien aus doctor und update (E115), Spalten wie die übrigen Berichtszeilen. */
1208
+ function sopsBerichtZeilen(befunde, quellen) {
1209
+ return befunde.map((befund) => {
1210
+ const ort = `${befund.datei} aus ${quellenAnzeigeName({ quelle: quelleAlsText(befund.checkout.quelle), ref: befund.checkout.ref }, quellen)}`;
1211
+ if (befund.ergebnis === 'repariert')
1212
+ return ` entschlüsselt ${ort}`;
1213
+ if (befund.ergebnis === 'verschluesselt')
1214
+ return ` verschlüsselt ${ort}: ${sopsGrundText(befund.grund)}`;
1215
+ return ` fehlgeschlagen ${ort}: ${befund.fehler}`;
1216
+ });
1217
+ }
1166
1218
  /** Eine Ereigniszeile im Verlauf (E80), eingerückt unter der Kopfzeile ihres Laufs. Checkouts stellt `checkoutGruppen` dar (E109). */
1167
- function verlaufEreignisZeile(ereignis) {
1219
+ function verlaufEreignisZeile(ereignis, quellen) {
1220
+ if (ereignis.art === 'sops-verschluesselt' || ereignis.art === 'sops-repariert')
1221
+ return sopsZeile(ereignis, quellen);
1168
1222
  if (ereignis.art === 'bundle') {
1169
1223
  const teile = [
1170
1224
  ereignis.gelegt.length > 0 ? `gelegt ${ereignis.gelegt.join(', ')}` : undefined,
@@ -59,3 +59,20 @@ export function verlaufZeitpunkt(iso, jetzt = new Date()) {
59
59
  return `Gestern, ${uhr}`;
60
60
  return `${zwei(datum.getDate())}.${zwei(datum.getMonth() + 1)}.${datum.getFullYear()}, ${uhr}`;
61
61
  }
62
+ /** Warum eine SOPS-Datei verschlüsselt blieb (E115), in Alltagssprache. GUI: renderer.js, sopsGrundText. */
63
+ export function sopsGrundText(grund) {
64
+ if (grund === 'sops-fehlt')
65
+ return 'sops ist auf diesem Rechner nicht installiert';
66
+ if (grund === 'lokale-aenderung')
67
+ return 'die Datei ist im Checkout von Hand geändert';
68
+ if (grund === 'wrapper-fehlt')
69
+ return 'sops-clean.sh fehlt (weder in /usr/local/bin noch in ~/bin)';
70
+ return 'die Entschlüsselung klappt nicht, vermutlich fehlt der passende Schlüssel';
71
+ }
72
+ /** Verlaufszeile zu einer SOPS-Datei (E115): verschlüsselt geblieben oder nachträglich entschlüsselt. */
73
+ export function sopsZeile(ereignis, quellen) {
74
+ const ort = `${ereignis.datei} aus ${quellenAnzeigeName(ereignis, quellen)}`;
75
+ return ereignis.grund === undefined
76
+ ? `Verschlüsselte Datei ${ort} entschlüsselt`
77
+ : `Datei ${ort} bleibt verschlüsselt: ${sopsGrundText(ereignis.grund)}`;
78
+ }
package/dist/gui/main.js CHANGED
@@ -174,6 +174,9 @@ function zielZustandSerialisieren(z) {
174
174
  bundles: z.bundles.map((b) => ({
175
175
  name: b.name,
176
176
  skills: b.skills,
177
+ // E114 L3: nicht verlinkte Mitglieder (herausgenommen oder von Hand gelöscht).
178
+ nichtVerlinkt: b.nichtVerlinkt,
179
+ herausgenommen: b.herausgenommen,
177
180
  quelleText: quelleAlsText(b.quelle),
178
181
  quelleName: quelleName(b.quelle),
179
182
  })),
@@ -234,8 +237,10 @@ function registriereIpc() {
234
237
  return ergebnis;
235
238
  }));
236
239
  // Bundles aller bekannten Quellen (E69): flach wie der Skill-Katalog, je Bundle seine Quelle und
237
- // die Zahl seiner Mitglieder. Kaputte Definitionen kommen als eigene Liste mit, damit die GUI sie
238
- // nennen kann, statt sie zu verschlucken.
240
+ // seine Mitglieder als Quelle plus Pfad (E114 L1). Name und Beschreibung eines Mitglieds sucht der
241
+ // Renderer im Skill-Katalog nach (`quelleSchluessel` + `pfad`): eine noch nicht eingerichtete
242
+ // Quelle wird hier nicht gelesen, sonst klonte jedes Öffnen des Dialogs. Kaputte Definitionen
243
+ // kommen als eigene Liste mit, damit die GUI sie nennen kann, statt sie zu verschlucken.
239
244
  ipcMain.handle('hmgr:bundles', async () => auffangen(async () => {
240
245
  const bundles = [];
241
246
  const kaputte = [];
@@ -244,7 +249,11 @@ function registriereIpc() {
244
249
  bundles.push({
245
250
  name: bundle.name,
246
251
  beschreibung: bundle.beschreibung,
247
- mitglieder: bundle.mitglieder.length,
252
+ mitglieder: bundle.mitglieder.map((m) => ({
253
+ quelleSchluessel: quelleSchluessel(m.quelle),
254
+ quelleText: quelleAlsText(m.quelle),
255
+ pfad: m.pfad,
256
+ })),
248
257
  // Anzeige im Dialog: aus welchen Quellen das Bundle Skills holt, ohne Dubletten.
249
258
  quellen: [...new Set(bundle.mitglieder.map((m) => quelleAlsText(m.quelle)))],
250
259
  quelleText: quelleAlsText(bundle.quelle),
@@ -272,6 +281,24 @@ function registriereIpc() {
272
281
  };
273
282
  }));
274
283
  ipcMain.handle('hmgr:bundleEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.bundleEntfernen({ bundle: args.bundle, ziel: zielAusIpc(args.harness, args.ort) })));
284
+ // Vervollständigen (E114 L4, `aufnehmen: 'alle'`) und „Wieder verlinken" einzelner Mitglieder (L3,
285
+ // Namensliste): der Abgleich dieses einen Bundles in diesem Ziel. Wie bundleEntfernen ohne Zeiger.
286
+ // Der Abgleich meldet einen Fehlschlag im Ergebnis statt zu werfen; hier wird er zum Fehler, damit
287
+ // `rufe` im Renderer ihn zeigt.
288
+ ipcMain.handle('hmgr:bundleAbgleichen', async (_event, args) => auffangen(async () => {
289
+ const eintraege = await anwendungsfaelle.bundleAbgleich({
290
+ bundle: args.bundle,
291
+ quelle: quelleAusText(args.quelleText),
292
+ ziel: zielAusIpc(args.harness, args.ort),
293
+ aufnehmen: args.aufnehmen,
294
+ });
295
+ const eintrag = eintraege[0];
296
+ if (eintrag === undefined)
297
+ throw new Error(`Bundle „${args.bundle}" ist dort nicht installiert.`);
298
+ if (eintrag.ergebnis === 'fehlgeschlagen')
299
+ throw new Error(`Bundle „${args.bundle}" ließ sich nicht vervollständigen: ${eintrag.fehler ?? ''}`);
300
+ return { gelegt: eintrag.gelegt };
301
+ }));
275
302
  // Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
276
303
  ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
277
304
  art: quelle.art,
@@ -858,6 +885,7 @@ function fensterOeffnen() {
858
885
  // fällt, was ins Ziel kommt, und zeigt je Bundle Beschreibung und Herkunft.
859
886
  var dskBundleBoxSichtbar = false;
860
887
  var dskBundleText = null;
888
+ var dskBundleZuVorher = null, dskBundleKoerperText = null;
861
889
  // Aufklappbereich je Zeile (E92): zu vor dem Klick, danach sichtbar mit der ungekürzten
862
890
  // Beschreibung; die Checkbox bleibt unberührt, der Dialog offen.
863
891
  var dskKoerperVorher = null;
@@ -895,6 +923,13 @@ function fensterOeffnen() {
895
923
  dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
896
924
  dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
897
925
  ?.replace(/\\s+/g, ' ').trim() ?? null;
926
+ // E114 L1: die Bundle-Wahlzeile klappt auf und zeigt ihre Skills samt Beschreibung.
927
+ var dskBundleKoerper = document.querySelector('#dsk-bundles .wahlzeile .wahlkoerper');
928
+ dskBundleZuVorher = dskBundleKoerper?.hidden ?? null;
929
+ document.querySelector('#dsk-bundles .wahlzeile .aufklappknopf')?.click();
930
+ dskBundleKoerperText = dskBundleKoerper && !dskBundleKoerper.hidden
931
+ ? dskBundleKoerper.textContent?.replace(/\\s+/g, ' ').trim() ?? null
932
+ : null;
898
933
  document.getElementById('d-skills')?.close();
899
934
  }
900
935
  // Verlinken-Dialog „Wohin?" (E76): er bietet nur Ziele aktiver Harnesses an. Der Text
@@ -908,20 +943,33 @@ function fensterOeffnen() {
908
943
  ?.replace(/\\s+/g, ' ').trim() ?? null;
909
944
  document.getElementById('d-ziel')?.close();
910
945
  }
911
- // Bundle-Gruppe an der Ziel-Karte (E69): Kopf mit Name, Quelle und Entfernen, darunter
912
- // die Zeilen der Mitglieder — sie stehen in der Gruppe, nicht in der allgemeinen Liste.
913
- var bundleGruppeText = document.querySelector('#zielliste .bundlegruppe .bundlekopf')?.textContent
946
+ // Bundle-Gruppe an der Claude-Code-Home-Karte (E69, E114): Kopf mit Name, Quelle, Zählung
947
+ // „1 von 2 Skills", „Vervollständigen" und „Entfernen"; darunter zugeklappt die Zeilen
948
+ // aller Mitglieder. „skill-creator" hat die Fixture herausgenommen, es steht als blasse
949
+ // Zeile „nicht verlinkt" mit „Wieder verlinken" in der Gruppe.
950
+ var homeGruppe = Array.from(document.querySelectorAll('#zielliste .ziel'))
951
+ .find((k) => (k.querySelector('.zielkopf')?.textContent || '').indexOf('Claude Code · Home') > -1)
952
+ ?.querySelector('.bundlegruppe') ?? null;
953
+ var bundleGruppeText = homeGruppe?.querySelector('.bundlekopf')?.textContent
914
954
  ?.replace(/\\s+/g, ' ').trim() ?? null;
915
- var bundleGruppeZeilen = Array.from(document.querySelectorAll('#zielliste .bundlegruppe .linkliste .name'))
955
+ var bundleGruppeZeilen = Array.from(homeGruppe?.querySelectorAll('.linkliste li:not(.nichtverlinkt) .name') ?? [])
916
956
  .map((n) => n.textContent.trim()).join(',');
917
- // Entfernen an der Einzelzeile innerhalb der Gruppe sagt, dass das Bundle zerbricht.
918
- var bundleZerbrichtText = null;
957
+ var bundleNichtVerlinktText = homeGruppe?.querySelector('.linkliste li.nichtverlinkt')?.textContent
958
+ ?.replace(/\\s+/g, ' ').trim() ?? null;
959
+ var bundleVervollstaendigenKnopf = Array.from(homeGruppe?.querySelectorAll('.bundlekopf button') ?? [])
960
+ .some((k) => k.textContent === 'Vervollständigen');
961
+ var bundleKoerperZuVorher = homeGruppe?.querySelector('.linkliste')?.hidden ?? null;
962
+ homeGruppe?.querySelector('.bundlekopf .aufklappknopf')?.click();
963
+ var bundleKoerperAufNachKlick = homeGruppe?.querySelector('.linkliste')?.hidden === false;
964
+ // Entfernen an der Mitgliedszeile sagt, dass der Skill herausgenommen wird und das
965
+ // Bundle bleibt (E114 L2, früher: zerbricht).
966
+ var bundleHerausnehmenText = null;
919
967
  try {
920
- document.querySelector('#zielliste .bundlegruppe .linkliste .knopf')?.click();
921
- bundleZerbrichtText = document.getElementById('dx-text')?.textContent ?? null;
968
+ homeGruppe?.querySelector('.linkliste li:not(.nichtverlinkt) .knopf')?.click();
969
+ bundleHerausnehmenText = document.getElementById('dx-text')?.textContent ?? null;
922
970
  document.querySelector('#d-entfernen button[value="nein"]')?.click();
923
971
  } catch (fehler) {
924
- bundleZerbrichtText = String(fehler);
972
+ bundleHerausnehmenText = String(fehler);
925
973
  }
926
974
  // Sammelstreifen unter „Alle" (E95): das Bundle „vorhaben" liegt an kunde-x in Claude Code
927
975
  // und Hermes — ein Streifen mit beiden Harness-Zeichen, Entfernen nennt beide Harnesses
@@ -932,12 +980,21 @@ function fensterOeffnen() {
932
980
  var streifenZahl = null, streifenHarnesse = null, streifenText = null;
933
981
  var streifenDialogTitel = null, streifenDialogText = null;
934
982
  var chipGruppeZahl = null, chipGruppeZeilenZahl = null, chipStreifenZahl = null;
983
+ // E114 L1: unter „Alle" stehen die Mitglieder in der Gruppe und nicht zusätzlich in der
984
+ // Tabelle. Name ohne Harness-Zeichen: nur die Textknoten der Namenszelle.
985
+ var nurName = (n) => Array.from(n.childNodes).filter((t) => t.nodeType === 3).map((t) => t.textContent).join('').trim();
986
+ var alleGruppeZeilen = null, alleTabelleZeilen = null;
935
987
  try {
936
988
  var streifen = Array.from(streifenKarte?.querySelectorAll('.bundlegruppe') ?? []);
937
989
  streifenZahl = streifen.length;
938
990
  streifenHarnesse = streifen[0]?.querySelector('.zeilenzeichen')?.getAttribute('aria-label') ?? null;
939
- streifenText = streifen[0]?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
940
- streifen[0]?.querySelector('button')?.click();
991
+ streifenText = streifen[0]?.querySelector('.bundlekopf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
992
+ alleGruppeZeilen = Array.from(streifen[0]?.querySelectorAll('.linkliste li .name') ?? []).map(nurName).join(',');
993
+ alleTabelleZeilen = Array.from(streifenKarte?.querySelectorAll('.linkliste li') ?? [])
994
+ .filter((li) => li.closest('.bundlegruppe') === null && li.querySelector('.name'))
995
+ .map((li) => nurName(li.querySelector('.name'))).join(',');
996
+ Array.from(streifen[0]?.querySelectorAll('.bundlekopf button') ?? [])
997
+ .find((k) => k.textContent === 'Entfernen')?.click();
941
998
  streifenDialogTitel = document.getElementById('dx-titel')?.textContent ?? null;
942
999
  streifenDialogText = document.getElementById('dx-text')?.textContent ?? null;
943
1000
  document.querySelector('#d-entfernen button[value="nein"]')?.click();
@@ -1236,13 +1293,21 @@ function fensterOeffnen() {
1236
1293
  dskAufklappFreiAngehakt,
1237
1294
  dskBundleBoxSichtbar,
1238
1295
  dskBundleText,
1296
+ dskBundleZuVorher,
1297
+ dskBundleKoerperText,
1239
1298
  dzZieleText,
1240
1299
  bundleGruppeText,
1241
1300
  bundleGruppeZeilen,
1242
- bundleZerbrichtText,
1301
+ bundleNichtVerlinktText,
1302
+ bundleVervollstaendigenKnopf,
1303
+ bundleKoerperZuVorher,
1304
+ bundleKoerperAufNachKlick,
1305
+ bundleHerausnehmenText,
1243
1306
  streifenZahl,
1244
1307
  streifenHarnesse,
1245
1308
  streifenText,
1309
+ alleGruppeZeilen,
1310
+ alleTabelleZeilen,
1246
1311
  streifenDialogTitel,
1247
1312
  streifenDialogText,
1248
1313
  chipGruppeZahl,
@@ -15,6 +15,8 @@ contextBridge.exposeInMainWorld('hmgr', {
15
15
  bundles: () => ipcRenderer.invoke('hmgr:bundles'),
16
16
  bundleInstallieren: (args) => ipcRenderer.invoke('hmgr:bundleInstallieren', args),
17
17
  bundleEntfernen: (args) => ipcRenderer.invoke('hmgr:bundleEntfernen', args),
18
+ /** Vervollständigen ('alle') oder einzelne Mitglieder wieder verlinken (E114). */
19
+ bundleAbgleichen: (args) => ipcRenderer.invoke('hmgr:bundleAbgleichen', args),
18
20
  /** Icon einer Quelle als data-URL, oder null (E60). */
19
21
  icon: (text) => ipcRenderer.invoke('hmgr:icon', text),
20
22
  /** Öffnet eine Quelle: Repo-Seite im Browser oder Ordner im Dateimanager (E60). */