hmgr 0.0.45 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/zustand-json.js +2 -1
- package/dist/aenderungen.json +3 -0
- package/dist/cli/hilfe.js +10 -3
- package/dist/cli/main.js +29 -7
- package/dist/gui/main.js +80 -15
- package/dist/gui/preload.cjs +2 -0
- package/dist/gui/renderer/renderer.js +309 -88
- package/dist/gui/renderer/styles.css +8 -0
- package/dist/kern/anwendungsfaelle.js +131 -45
- package/package.json +1 -1
|
@@ -18,7 +18,8 @@ function deuten(roh) {
|
|
|
18
18
|
aktiveHarnesses: (daten.aktiveHarnesses ?? ['claude-code']),
|
|
19
19
|
// Fehlt das Feld (state.json vor E43), gilt: nichts ignoriert.
|
|
20
20
|
ignorierteEintraege: (daten.ignorierteEintraege ?? []),
|
|
21
|
-
// Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert.
|
|
21
|
+
// Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert. Ein Eintrag ohne
|
|
22
|
+
// `herausgenommen` (vor E114) hat keine herausgenommenen Mitglieder, das Feld bleibt dann weg.
|
|
22
23
|
bundleInstallationen: (daten.bundleInstallationen ?? []),
|
|
23
24
|
// Fehlt das Feld (state.json vor E78), gilt: jeder Harness hat nur sein Standard-Homedir.
|
|
24
25
|
harnessHeime: (daten.harnessHeime ?? []),
|
package/dist/aenderungen.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"fassung": 1,
|
|
3
3
|
"versionen": {
|
|
4
|
+
"0.0.46": [
|
|
5
|
+
"Bundles lassen sich aufklappen und zeigen ihre Skills mit Beschreibung, auch schon im Dialog „Skills hinzufügen“. Entfernst du einen einzelnen Skill, bleibt das Bundle bestehen und zeigt, was fehlt; mit „Vervollständigen“ oder „Wieder verlinken“ holst du Skills zurück, und „Entfernen“ am Bundle räumt den Rest weg."
|
|
6
|
+
],
|
|
4
7
|
"0.0.45": [
|
|
5
8
|
"MCP-Server und die Listen in den Einstellungen (Harnesses, zusätzliche Homedirs) erscheinen jetzt alphabetisch sortiert, ebenso in hmgr mcp list."
|
|
6
9
|
],
|
package/dist/cli/hilfe.js
CHANGED
|
@@ -315,9 +315,13 @@ Liegt am Ziel schon derselbe Skill aus derselben Quelle als hmgr-Link, wird er a
|
|
|
315
315
|
übernommen: aus Bestand wird ein Bundle, ohne erst Links von Hand zu entfernen. Alles andere unter
|
|
316
316
|
demselben Namen ist ein Konflikt und wird genannt.
|
|
317
317
|
|
|
318
|
-
hmgr unlink <skill> auf ein einzelnes Mitglied
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
hmgr unlink <skill> auf ein einzelnes Mitglied nimmt es aus dem Bundle heraus: nur dieser Link
|
|
319
|
+
verschwindet, das Bundle bleibt im Ziel und merkt sich den Skill als herausgenommen. sync und
|
|
320
|
+
update legen ihn nicht wieder an, neue Mitglieder der Definition ergänzen sie weiter. Zurück kommt
|
|
321
|
+
er mit hmgr link <skill> desselben Skills aus derselben Quelle oder mit bundle sync --complete, das
|
|
322
|
+
alle fehlenden Mitglieder verlinkt. remove nimmt die noch verlinkten Mitglieder weg, auch wenn
|
|
323
|
+
schon welche herausgenommen sind. hmgr status und bundle list zeigen die Lage je Ziel, etwa
|
|
324
|
+
„4/5 … fehlt: x".
|
|
321
325
|
|
|
322
326
|
Den Abgleich macht schon hmgr update für alle installierten Bundles mit, es ist derselbe Vorgang
|
|
323
327
|
und dieselbe Ausgabe (E69): eine geänderte Definition kommt also mit dem gewöhnlichen
|
|
@@ -335,11 +339,14 @@ Schalter (add, remove und sync):
|
|
|
335
339
|
kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
|
|
336
340
|
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
337
341
|
--quelle <teil> nur bei add: Quelle eingrenzen, wenn den Namen mehrere Quellen führen
|
|
342
|
+
--complete nur bei sync: auch herausgenommene Mitglieder verlinken (Vervollständigen),
|
|
343
|
+
die Liste der herausgenommenen ist danach leer. Auch --vervollstaendigen
|
|
338
344
|
|
|
339
345
|
Beispiele:
|
|
340
346
|
hmgr bundle list
|
|
341
347
|
hmgr bundle add wordpress --ort dir:.
|
|
342
348
|
hmgr bundle sync
|
|
349
|
+
hmgr bundle sync wordpress --ort dir:. --complete
|
|
343
350
|
hmgr bundle remove wordpress --ort dir:.
|
|
344
351
|
|
|
345
352
|
Häufige Fälle:
|
package/dist/cli/main.js
CHANGED
|
@@ -125,7 +125,7 @@ const SCHALTER = {
|
|
|
125
125
|
unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
|
|
126
126
|
adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
|
|
127
127
|
plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
|
|
128
|
-
bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: [] },
|
|
128
|
+
bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['complete', 'vervollstaendigen'] },
|
|
129
129
|
mcp: { werte: ['harness', 'ort'], flaggen: [] },
|
|
130
130
|
sync: { werte: [], flaggen: [] },
|
|
131
131
|
update: { werte: ['quelle'], flaggen: [] },
|
|
@@ -160,7 +160,8 @@ const UNTERKOMMANDOS = {
|
|
|
160
160
|
add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle'] },
|
|
161
161
|
remove: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
|
|
162
162
|
// sync nimmt einen Bundle-Namen, aber keinen zwingend: ohne Namen gilt der ganze Bestand.
|
|
163
|
-
|
|
163
|
+
// --complete/--vervollstaendigen holt herausgenommene Mitglieder zurück (E114 L4).
|
|
164
|
+
sync: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'complete', 'vervollstaendigen'] },
|
|
164
165
|
list: { woerter: 1, schalter: [] },
|
|
165
166
|
},
|
|
166
167
|
mcp: {
|
|
@@ -491,9 +492,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
491
492
|
if (kommando === 'unlink') {
|
|
492
493
|
const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
|
|
493
494
|
console.log(`Link ${skill} entfernt.`);
|
|
494
|
-
//
|
|
495
|
-
for (const bundle of ergebnis.
|
|
496
|
-
console.log(
|
|
495
|
+
// Herausgenommen (E114 L2): das Bundle bleibt, der Abgleich legt den Skill nicht wieder an.
|
|
496
|
+
for (const bundle of ergebnis.herausgenommenAus)
|
|
497
|
+
console.log(`„${skill}" ist aus dem Bundle „${bundle}" herausgenommen, das Bundle bleibt. ` +
|
|
498
|
+
`Zurück mit hmgr link ${skill} oder hmgr bundle sync ${bundle} --complete.`);
|
|
497
499
|
if (ergebnis.hinweis !== undefined)
|
|
498
500
|
console.log(ergebnis.hinweis);
|
|
499
501
|
return 0;
|
|
@@ -612,10 +614,15 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
612
614
|
if (kommando === 'bundle') {
|
|
613
615
|
const schalter = schalterLesen(argv.slice(1), 'bundle');
|
|
614
616
|
unterkommandoPruefen('bundle', schalter);
|
|
615
|
-
const { rest, werte } = schalter;
|
|
617
|
+
const { rest, werte, flaggen } = schalter;
|
|
616
618
|
const unterkommando = rest[0];
|
|
617
619
|
if (unterkommando === 'list') {
|
|
618
620
|
const katalog = await anwendungsfaelle.bundleKatalog();
|
|
621
|
+
// Je Bundle die Ziele, in denen es liegt, mit seiner Lage (E114 L6). Nur nötig, wenn
|
|
622
|
+
// überhaupt ein Bundle installiert ist; der Ist-Zustand kostet sonst einen Scan aller Ziele.
|
|
623
|
+
const installiert = (await ports.zustand.lesen()).bundleInstallationen.length > 0
|
|
624
|
+
? (await anwendungsfaelle.istZustand({ plugins: false })).flatMap((z) => z.bundles.map((b) => ({ ziel: z.ziel, bundle: b })))
|
|
625
|
+
: [];
|
|
619
626
|
const leer = katalog.every((k) => k.bundles.length === 0 && k.kaputte.length === 0 && k.fehler === undefined);
|
|
620
627
|
if (leer)
|
|
621
628
|
console.log('Keine Quelle mit Bundles. hmgr help bundle zeigt das Format von bundles/<name>.yaml.');
|
|
@@ -631,6 +638,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
631
638
|
console.log(` ${bundle.name} ${kappen(bundle.beschreibung)}`);
|
|
632
639
|
for (const mitglied of bundle.mitglieder)
|
|
633
640
|
console.log(` ${mitglied.pfad} aus ${quelleAlsText(mitglied.quelle)}`);
|
|
641
|
+
for (const { ziel, bundle: lage } of installiert)
|
|
642
|
+
if (lage.name === bundle.name && quelleSchluessel(lage.quelle) === quelleSchluessel(bundle.quelle))
|
|
643
|
+
console.log(` installiert ${ziel.harness} ${beschreibeOrt(ziel.ort)} ${bundleLage(lage)}`);
|
|
634
644
|
}
|
|
635
645
|
// Eine kaputte Definition verdeckt die übrigen nicht, sie wird eigens genannt (E69).
|
|
636
646
|
for (const kaputt of eintrag.kaputte)
|
|
@@ -683,9 +693,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
683
693
|
}
|
|
684
694
|
// sync: ohne Ziel-Schalter der ganze Bestand, mit --ort/--harness nur dieses Ziel.
|
|
685
695
|
const nurZiel = werte.has('harness') || werte.has('ort');
|
|
696
|
+
const vervollstaendigen = flaggen.has('complete') || flaggen.has('vervollstaendigen');
|
|
686
697
|
const eintraege = await anwendungsfaelle.bundleAbgleich({
|
|
687
698
|
...(name === undefined ? {} : { bundle: name }),
|
|
688
699
|
...(nurZiel ? { ziel } : {}),
|
|
700
|
+
...(vervollstaendigen ? { aufnehmen: 'alle' } : {}),
|
|
689
701
|
});
|
|
690
702
|
if (eintraege.length === 0)
|
|
691
703
|
console.log('Kein Bundle installiert, nichts abzugleichen.');
|
|
@@ -888,7 +900,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
888
900
|
}
|
|
889
901
|
// Bundles des Ziels (E69), unter den Einträgen: welche Links als Einheit zusammengehören.
|
|
890
902
|
for (const bundle of zielZustand.bundles)
|
|
891
|
-
console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundle
|
|
903
|
+
console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundleLage(bundle)}`);
|
|
892
904
|
if (zielZustand.plugins !== undefined)
|
|
893
905
|
console.log(` Plugins: ${zielZustand.plugins.length === 0 ? 'keine' : zielZustand.plugins.join(', ')}`);
|
|
894
906
|
}
|
|
@@ -1134,6 +1146,16 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
1134
1146
|
}
|
|
1135
1147
|
throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
|
|
1136
1148
|
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Lage eines installierten Bundles in einem Ziel (E114 L6), für `status` und `bundle list`:
|
|
1151
|
+
* verlinkt von gesamt, die verlinkten Namen und, wenn etwas fehlt, was.
|
|
1152
|
+
*/
|
|
1153
|
+
function bundleLage(bundle) {
|
|
1154
|
+
const gesamt = bundle.skills.length + bundle.nichtVerlinkt.length;
|
|
1155
|
+
const verlinkt = bundle.skills.length === 0 ? '' : ` ${bundle.skills.join(', ')}`;
|
|
1156
|
+
const fehlt = bundle.nichtVerlinkt.length === 0 ? '' : ` fehlt: ${bundle.nichtVerlinkt.join(', ')}`;
|
|
1157
|
+
return `${bundle.skills.length}/${gesamt}${verlinkt}${fehlt}`;
|
|
1158
|
+
}
|
|
1137
1159
|
/**
|
|
1138
1160
|
* Bericht über den Bundle-Abgleich (E69), gleich für `hmgr bundle sync` und `hmgr update`: je
|
|
1139
1161
|
* Bundle eine Zeile mit seinem Ausgang, darunter, was sich geändert hat. „fehlgeschlagen" heißt
|
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
|
-
//
|
|
238
|
-
//
|
|
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.
|
|
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
|
|
912
|
-
//
|
|
913
|
-
|
|
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(
|
|
955
|
+
var bundleGruppeZeilen = Array.from(homeGruppe?.querySelectorAll('.linkliste li:not(.nichtverlinkt) .name') ?? [])
|
|
916
956
|
.map((n) => n.textContent.trim()).join(',');
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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
|
-
|
|
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]?.
|
|
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
|
-
|
|
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,
|
package/dist/gui/preload.cjs
CHANGED
|
@@ -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). */
|
|
@@ -745,7 +745,7 @@
|
|
|
745
745
|
}
|
|
746
746
|
|
|
747
747
|
/** Eine Zeile der Link-Tabelle. `bundle` gesetzt heißt: die Zeile steht in einer Bundle-Gruppe
|
|
748
|
-
(E69), das Entfernen
|
|
748
|
+
(E69), das Entfernen nimmt den Skill dort aus dem Bundle heraus und der Dialog sagt es (E114). */
|
|
749
749
|
function linkZeile(ei, z, bundle) {
|
|
750
750
|
var name = ei.art === 'link' ? ei.link.name : ei.name;
|
|
751
751
|
var li = e('li');
|
|
@@ -801,63 +801,156 @@
|
|
|
801
801
|
return li;
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
-
/**
|
|
805
|
-
|
|
806
|
-
function
|
|
804
|
+
/** Alle Mitglieder eines installierten Bundles in einem Ziel (E114 L3): verlinkte und nicht
|
|
805
|
+
verlinkte, aus dem Zustand — ohne die Definition, die Gruppe steht auch bei fehlender Quelle. */
|
|
806
|
+
function bundleAlleMitglieder(b) {
|
|
807
|
+
return b.skills.concat(b.nichtVerlinkt || []);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** Skill zu einem Mitglied einer Definition aus dem Skill-Katalog (E114 L1): gleiche Quelle, gleicher
|
|
811
|
+
Pfad. null, wenn die Quelle (noch) nicht eingerichtet oder nicht lesbar ist. */
|
|
812
|
+
function bundleMitgliedSkill(m) {
|
|
813
|
+
return SKILLS.filter(function (s) { return s.quelleSchluessel === m.quelleSchluessel && s.pfad === m.pfad; })[0] || null;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Skill eines nicht verlinkten Mitglieds über seinen Namen, gesucht unter den Mitgliedern der
|
|
817
|
+
Definition (Name plus Quelle des Bundles). null, wenn sich das nicht auflösen lässt. */
|
|
818
|
+
function bundleSkillNachName(b, name) {
|
|
819
|
+
var katalog = BUNDLES.filter(function (k) { return k.name === b.name && k.quelleText === b.quelleText; })[0];
|
|
820
|
+
if (!katalog) return null;
|
|
821
|
+
for (var i = 0; i < katalog.mitglieder.length; i++) {
|
|
822
|
+
var skill = bundleMitgliedSkill(katalog.mitglieder[i]);
|
|
823
|
+
if (skill && skill.name === name) return skill;
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** Vervollständigen (E114 L4, `aufnehmen` 'alle') oder Wieder verlinken (L3, Namensliste) eines
|
|
829
|
+
Bundles in den genannten Zielen. Ein Fehlschlag in einem Ziel hält die übrigen nicht auf,
|
|
830
|
+
`rufe` meldet ihn selbst. */
|
|
831
|
+
async function bundleAufnehmen(b, ziele, aufnehmen) {
|
|
832
|
+
var gelegt = 0;
|
|
833
|
+
var gescheitert = 0;
|
|
834
|
+
for (var i = 0; i < ziele.length; i++) {
|
|
835
|
+
var ziel = ziele[i];
|
|
836
|
+
try {
|
|
837
|
+
var ergebnis = await rufe(function () {
|
|
838
|
+
return window.hmgr.bundleAbgleichen({
|
|
839
|
+
bundle: b.name, quelleText: b.quelleText, harness: ziel.harness, ort: ziel.ort, aufnehmen: aufnehmen,
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
gelegt += ergebnis.gelegt.length;
|
|
843
|
+
} catch (fehler) {
|
|
844
|
+
gescheitert++;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
await ladeAlles();
|
|
848
|
+
if (gescheitert === 0) melde('Bundle ' + b.name + ': ' + zahlwort(gelegt, 'Link', 'Links') + ' gelegt.');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Aufklappzustand der Bundle-Gruppen (E114 L1), überlebt den Neuaufbau durch ladeAlles wie
|
|
852
|
+
// SKILL_OFFEN. Zu Beginn sind die Gruppen zugeklappt.
|
|
853
|
+
var BUNDLE_OFFEN = {};
|
|
854
|
+
var bundleKoerperNr = 0;
|
|
855
|
+
|
|
856
|
+
/** Rahmen einer Bundle-Gruppe (E69, E114 L1/L3/L4): Kopf mit Name, Quelle, „x von N Skills",
|
|
857
|
+
„Vervollständigen", wenn etwas fehlt, und „Entfernen" für das Ganze; darunter aufklappbar die
|
|
858
|
+
Zeilen aller Mitglieder. `harnesse` nur unter „Alle" (Harness-Zeichen am Kopf, E95). */
|
|
859
|
+
function bundleRahmen(b, harnesse, verlinkt, gesamt, ziele, zeilen, entfernen, schluessel) {
|
|
807
860
|
var gruppe = e('div', 'bundlegruppe');
|
|
808
861
|
var kopf = e('div', 'bundlekopf');
|
|
809
862
|
var titel = e('span', 'bundletitel');
|
|
863
|
+
if (harnesse) titel.appendChild(harnesseZeichenSammel(harnesse));
|
|
810
864
|
titel.appendChild(e('span', 'etikett', 'Bundle'));
|
|
811
865
|
titel.appendChild(e('b', 'bundlename', b.name));
|
|
812
866
|
kopf.appendChild(titel);
|
|
813
867
|
var quelleEl = e('span', 'mono bundlequelle', b.quelleName);
|
|
814
868
|
quelleEl.title = b.quelleText;
|
|
815
869
|
kopf.appendChild(quelleEl);
|
|
816
|
-
kopf.appendChild(e('span', 'bundlezahl',
|
|
870
|
+
kopf.appendChild(e('span', 'bundlezahl', verlinkt === gesamt
|
|
871
|
+
? zahlwort(gesamt, 'Skill', 'Skills')
|
|
872
|
+
: verlinkt + ' von ' + zahlwort(gesamt, 'Skill', 'Skills')));
|
|
873
|
+
var aktionen = e('span', 'bundleaktionen');
|
|
874
|
+
if (verlinkt < gesamt) {
|
|
875
|
+
var voll = e('button', 'knopf leise klein', 'Vervollständigen');
|
|
876
|
+
voll.type = 'button';
|
|
877
|
+
voll.setAttribute('aria-label', 'Bundle ' + b.name + ' vervollständigen: alle fehlenden Skills verlinken');
|
|
878
|
+
voll.addEventListener('click', function () { bundleAufnehmen(b, ziele, 'alle'); });
|
|
879
|
+
aktionen.appendChild(voll);
|
|
880
|
+
}
|
|
817
881
|
var weg = e('button', 'knopf leise klein', 'Entfernen');
|
|
818
882
|
weg.type = 'button';
|
|
819
|
-
weg.setAttribute('aria-label', 'Bundle ' + b.name + '
|
|
820
|
-
weg.addEventListener('click',
|
|
821
|
-
|
|
883
|
+
weg.setAttribute('aria-label', 'Bundle ' + b.name + ' entfernen');
|
|
884
|
+
weg.addEventListener('click', entfernen);
|
|
885
|
+
aktionen.appendChild(weg);
|
|
886
|
+
var offen = !!BUNDLE_OFFEN[schluessel];
|
|
887
|
+
var koerperId = 'bundlekoerper-' + bundleKoerperNr++;
|
|
888
|
+
var umschalter = e('button', 'aufklappknopf');
|
|
889
|
+
umschalter.type = 'button';
|
|
890
|
+
umschalter.setAttribute('aria-expanded', offen ? 'true' : 'false');
|
|
891
|
+
umschalter.setAttribute('aria-controls', koerperId);
|
|
892
|
+
umschalter.setAttribute('aria-label', 'Skills des Bundles ' + b.name + ' ein- oder ausklappen');
|
|
893
|
+
var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
894
|
+
pfeil.setAttribute('viewBox', '0 0 16 16');
|
|
895
|
+
pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
|
|
896
|
+
pfeil.setAttribute('aria-hidden', 'true');
|
|
897
|
+
pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
|
|
898
|
+
umschalter.appendChild(pfeil);
|
|
899
|
+
aktionen.appendChild(umschalter);
|
|
900
|
+
kopf.appendChild(aktionen);
|
|
822
901
|
gruppe.appendChild(kopf);
|
|
823
902
|
var ul = e('ul', 'linkliste');
|
|
824
|
-
|
|
903
|
+
ul.id = koerperId;
|
|
904
|
+
ul.hidden = !offen;
|
|
905
|
+
zeilen.forEach(function (li) { ul.appendChild(li); });
|
|
825
906
|
gruppe.appendChild(ul);
|
|
907
|
+
function schalte() {
|
|
908
|
+
offen = !offen;
|
|
909
|
+
BUNDLE_OFFEN[schluessel] = offen;
|
|
910
|
+
umschalter.setAttribute('aria-expanded', offen ? 'true' : 'false');
|
|
911
|
+
ul.hidden = !offen;
|
|
912
|
+
}
|
|
913
|
+
umschalter.addEventListener('click', schalte);
|
|
914
|
+
// Der Kopf schaltet mit, außer über seine Knöpfe (wie der Kopf einer Ziel-Karte).
|
|
915
|
+
kopf.addEventListener('click', function (ev) {
|
|
916
|
+
if (ev.target.closest('button, a') === null) schalte();
|
|
917
|
+
});
|
|
826
918
|
return gruppe;
|
|
827
919
|
}
|
|
828
920
|
|
|
829
|
-
/**
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
921
|
+
/** Zeile eines nicht verlinkten Mitglieds (E114 L3): blass, „nicht verlinkt", „Wieder verlinken".
|
|
922
|
+
`skill` aus dem Katalog oder null, wenn die Definition oder Quelle nicht lesbar ist. */
|
|
923
|
+
function nichtVerlinktZeile(name, skill, wiederVerlinken) {
|
|
924
|
+
var li = e('li', 'nichtverlinkt');
|
|
925
|
+
li.appendChild(e('span'));
|
|
926
|
+
li.appendChild(e('span', 'name', name));
|
|
927
|
+
if (skill) li.appendChild(wasZelle(skill));
|
|
928
|
+
else {
|
|
929
|
+
var feld = e('span', 'wasfeld');
|
|
930
|
+
feld.appendChild(e('span', 'was fehlt', 'Beschreibung nicht lesbar, die Quelle fehlt hier'));
|
|
931
|
+
li.appendChild(feld);
|
|
932
|
+
}
|
|
933
|
+
li.appendChild(e('span', 'quelle', 'nicht verlinkt'));
|
|
934
|
+
var knopf = e('button', 'knopf leise klein', 'Wieder verlinken');
|
|
935
|
+
knopf.type = 'button';
|
|
936
|
+
knopf.setAttribute('aria-label', name + ' wieder verlinken');
|
|
937
|
+
knopf.addEventListener('click', wiederVerlinken);
|
|
938
|
+
li.appendChild(knopf);
|
|
939
|
+
return li;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Ein installiertes Bundle unter dem Harness-Chip (E69, E114): alle Mitglieder in der Gruppe,
|
|
943
|
+
verlinkte als gewöhnliche Link-Zeilen, nicht verlinkte blass mit „Wieder verlinken". */
|
|
944
|
+
function bundleGruppe(b, z, eintraege) {
|
|
945
|
+
var zeilen = eintraege.map(function (ei) { return linkZeile(ei, z, b); });
|
|
946
|
+
(b.nichtVerlinkt || []).slice().sort(function (x, y) { return x.localeCompare(y, 'de'); }).forEach(function (name) {
|
|
947
|
+
zeilen.push(nichtVerlinktZeile(name, bundleSkillNachName(b, name), function () {
|
|
948
|
+
bundleAufnehmen(b, [z.ziel], [name]);
|
|
949
|
+
}));
|
|
857
950
|
});
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
951
|
+
return bundleRahmen(b, null, b.skills.length, bundleAlleMitglieder(b).length, [z.ziel], zeilen, function () {
|
|
952
|
+
frageBundleEntfernen(b, z.ziel);
|
|
953
|
+
}, 'ziel\u0000' + zielKey(z.ziel) + '\u0000' + b.name + '\u0000' + b.quelleText);
|
|
861
954
|
}
|
|
862
955
|
|
|
863
956
|
function linkTeil(z) {
|
|
@@ -881,8 +974,13 @@
|
|
|
881
974
|
}
|
|
882
975
|
}
|
|
883
976
|
|
|
977
|
+
// Bundles zuerst (E69): sie stehen als Gruppe über den Einzel-Zeilen, ihre Mitglieder liegen
|
|
978
|
+
// in der Gruppe statt in der allgemeinen Liste. Was zum Bundle gehört, sagt der Kern. Eine Gruppe
|
|
979
|
+
// steht auch, wenn kein Mitglied mehr verlinkt ist („0 von N", E114).
|
|
980
|
+
var bundles = (z.bundles || []).filter(function (b) { return bundleAlleMitglieder(b).length > 0; });
|
|
981
|
+
|
|
884
982
|
var fussContainer;
|
|
885
|
-
if (eigene.length === 0) {
|
|
983
|
+
if (eigene.length === 0 && bundles.length === 0) {
|
|
886
984
|
var leer = e('div', 'leerzeile');
|
|
887
985
|
leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
|
|
888
986
|
teil.appendChild(leer);
|
|
@@ -898,9 +996,6 @@
|
|
|
898
996
|
kopf.appendChild(e('span'));
|
|
899
997
|
teil.appendChild(kopf);
|
|
900
998
|
|
|
901
|
-
// Bundles zuerst (E69): sie stehen als Gruppe über den Einzel-Zeilen, ihre Mitglieder liegen
|
|
902
|
-
// in der Gruppe statt in der allgemeinen Liste. Was zum Bundle gehört, sagt der Kern.
|
|
903
|
-
var bundles = (z.bundles || []).filter(function (b) { return b.skills.length > 0; });
|
|
904
999
|
var bundleVon = {};
|
|
905
1000
|
bundles.forEach(function (b) { b.skills.forEach(function (n) { bundleVon[n] = b; }); });
|
|
906
1001
|
bundles
|
|
@@ -931,7 +1026,7 @@
|
|
|
931
1026
|
|
|
932
1027
|
// Ohne eigene Links (leer-Fall) steht der MCP-Abschnitt hinter der Leerzeile, die dort schon den
|
|
933
1028
|
// Kartenfuß bildet (Nachricht + Knöpfe in einer Zeile, wie überall sonst im leeren Zustand).
|
|
934
|
-
if (eigene.length === 0) {
|
|
1029
|
+
if (eigene.length === 0 && bundles.length === 0) {
|
|
935
1030
|
var mcpAbschnOhneTabelle = mcpAbschnitt(z.ziel);
|
|
936
1031
|
if (mcpAbschnOhneTabelle) teil.appendChild(mcpAbschnOhneTabelle);
|
|
937
1032
|
}
|
|
@@ -1220,7 +1315,37 @@
|
|
|
1220
1315
|
verzMcpZiele.push(z);
|
|
1221
1316
|
});
|
|
1222
1317
|
|
|
1223
|
-
|
|
1318
|
+
// Bundles unter „Alle" (E69, E95, E114 L1): eine aufklappbare Gruppe je Bundle, wie unter dem
|
|
1319
|
+
// Harness-Chip, aber über alle Harnesses am Ort zusammengefasst: Schlüssel ist Name plus Quelle,
|
|
1320
|
+
// gleichnamige Bundles aus zwei Quellen bleiben getrennt. Eine Gruppe steht auch, wenn kein
|
|
1321
|
+
// Mitglied mehr verlinkt ist („0 von N").
|
|
1322
|
+
var bundleGruppen = {}, bundleReihenfolge = [];
|
|
1323
|
+
teile.forEach(function (z) {
|
|
1324
|
+
(z.bundles || [])
|
|
1325
|
+
.filter(function (b) { return bundleAlleMitglieder(b).length > 0; })
|
|
1326
|
+
.forEach(function (b) {
|
|
1327
|
+
var schluessel = b.name + '\u0000' + b.quelleText;
|
|
1328
|
+
if (!bundleGruppen[schluessel]) { bundleGruppen[schluessel] = []; bundleReihenfolge.push(schluessel); }
|
|
1329
|
+
bundleGruppen[schluessel].push({ b: b, z: z });
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
// Eine Tabellenzeile geht in einer Gruppe auf, wenn ihr Name in JEDEM Harness der Zeile ein
|
|
1333
|
+
// verlinktes Mitglied dieses Bundles ist — dann steht sie nur in der Gruppe, nicht zusätzlich in
|
|
1334
|
+
// der Tabelle (E114 L1). Hängt derselbe Skill in einem Harness als Einzel-Link, bleibt die
|
|
1335
|
+
// Zeile in der Tabelle. Die Zählung im Kartenkopf (E71) bleibt davon unberührt.
|
|
1336
|
+
var zeileInGruppe = {};
|
|
1337
|
+
reihenfolge.forEach(function (key) {
|
|
1338
|
+
var row = zeilen[key];
|
|
1339
|
+
bundleReihenfolge.forEach(function (gk) {
|
|
1340
|
+
if (zeileInGruppe[key]) return;
|
|
1341
|
+
var ganz = row.teile.every(function (z) {
|
|
1342
|
+
return bundleGruppen[gk].some(function (t) { return t.z === z && t.b.skills.indexOf(row.name) > -1; });
|
|
1343
|
+
});
|
|
1344
|
+
if (ganz) zeileInGruppe[key] = gk;
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
if (reihenfolge.length === 0 && bundleReihenfolge.length === 0) {
|
|
1224
1349
|
var leer = e('div', 'leerzeile');
|
|
1225
1350
|
leer.appendChild(e('p', 'ohne', 'Hier liegt noch nichts.'));
|
|
1226
1351
|
leer.appendChild(alleSkillsKnopf(pfad, teile));
|
|
@@ -1236,27 +1361,6 @@
|
|
|
1236
1361
|
return teil;
|
|
1237
1362
|
}
|
|
1238
1363
|
|
|
1239
|
-
// Bundles unter „Alle" (E69): die Zeilen bleiben nach Name und Quelle gebündelt (E53), ein
|
|
1240
|
-
// Bundle gehört aber zu genau einem Harness — deshalb hier ein Streifen je Bundle mit seinem
|
|
1241
|
-
// Harness-Zeichen statt einer Umgruppierung der Tabelle. Unter dem Harness-Chip zeigt linkTeil
|
|
1242
|
-
// die volle Gruppe über den Zeilen.
|
|
1243
|
-
// Ein Bundle, das in mehreren Harnesses am selben Ort liegt, ist hier ein Streifen (E95), wie
|
|
1244
|
-
// die Skill-Zeilen: Schlüssel ist Name plus Quelle, gleichnamige Bundles aus zwei Quellen
|
|
1245
|
-
// bleiben getrennt.
|
|
1246
|
-
var bundleGruppen = {}, bundleReihenfolge = [];
|
|
1247
|
-
teile.forEach(function (z) {
|
|
1248
|
-
(z.bundles || [])
|
|
1249
|
-
.filter(function (b) { return b.skills.length > 0; })
|
|
1250
|
-
.forEach(function (b) {
|
|
1251
|
-
var schluessel = b.name + '\u0000' + b.quelleText;
|
|
1252
|
-
if (!bundleGruppen[schluessel]) { bundleGruppen[schluessel] = []; bundleReihenfolge.push(schluessel); }
|
|
1253
|
-
bundleGruppen[schluessel].push({ b: b, z: z });
|
|
1254
|
-
});
|
|
1255
|
-
});
|
|
1256
|
-
bundleReihenfolge
|
|
1257
|
-
.sort(function (a, b) { return bundleGruppen[a][0].b.name.localeCompare(bundleGruppen[b][0].b.name, 'de'); })
|
|
1258
|
-
.forEach(function (k) { teil.appendChild(bundleStreifen(bundleGruppen[k])); });
|
|
1259
|
-
|
|
1260
1364
|
var kopf = e('div', 'linkkopf');
|
|
1261
1365
|
kopf.appendChild(e('span'));
|
|
1262
1366
|
kopf.appendChild(e('span', 'etikett', 'Skill'));
|
|
@@ -1265,16 +1369,17 @@
|
|
|
1265
1369
|
kopf.appendChild(e('span'));
|
|
1266
1370
|
teil.appendChild(kopf);
|
|
1267
1371
|
|
|
1268
|
-
var
|
|
1269
|
-
reihenfolge
|
|
1372
|
+
var sortiert = reihenfolge
|
|
1270
1373
|
.slice()
|
|
1271
1374
|
.sort(function (ka, kb) {
|
|
1272
1375
|
var a = zeilen[ka], b = zeilen[kb];
|
|
1273
1376
|
if (a.defekt !== b.defekt) return a.defekt ? -1 : 1;
|
|
1274
1377
|
return a.name.localeCompare(b.name, 'de');
|
|
1275
|
-
})
|
|
1276
|
-
|
|
1277
|
-
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
/** Eine gebündelte Zeile unter „Alle" (E53). `bundleName` gesetzt: sie steht in einer
|
|
1381
|
+
Bundle-Gruppe, das Entfernen nimmt den Skill dort heraus (E114). */
|
|
1382
|
+
function alleZeile(row, bundleName) {
|
|
1278
1383
|
var harnesse = [];
|
|
1279
1384
|
row.teile.forEach(function (z) { if (harnesse.indexOf(z.ziel.harness) === -1) harnesse.push(z.ziel.harness); });
|
|
1280
1385
|
harnesse.sort(function (a, b) { return harnessName(a).localeCompare(harnessName(b), 'de'); });
|
|
@@ -1325,14 +1430,64 @@
|
|
|
1325
1430
|
// Namens nebeneinander, sobald ein Skill je Harness aus verschiedener Quelle hängt.
|
|
1326
1431
|
weg.setAttribute('aria-label', 'Link auf ' + row.name + ' in '
|
|
1327
1432
|
+ entfernbar.map(function (z) { return harnessName(z.ziel.harness); }).join(', ') + ' entfernen');
|
|
1328
|
-
weg.addEventListener('click', function () {
|
|
1433
|
+
weg.addEventListener('click', function () {
|
|
1434
|
+
frageEntfernenAlle(row.name, pfad, entfernbar, nichtEntfernbar, bundleName);
|
|
1435
|
+
});
|
|
1329
1436
|
li.appendChild(weg);
|
|
1330
1437
|
} else {
|
|
1331
1438
|
li.appendChild(e('span', 'kein-weg', 'nur über Plugin „' + pluginNamen.join('“, „') + '“'));
|
|
1332
1439
|
}
|
|
1333
|
-
|
|
1440
|
+
return li;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Erst die Bundle-Gruppen, darunter die übrigen Zeilen (wie unter dem Harness-Chip, E69).
|
|
1444
|
+
bundleReihenfolge
|
|
1445
|
+
.sort(function (a, b) { return bundleGruppen[a][0].b.name.localeCompare(bundleGruppen[b][0].b.name, 'de'); })
|
|
1446
|
+
.forEach(function (gk) {
|
|
1447
|
+
var treffer = bundleGruppen[gk];
|
|
1448
|
+
var b = treffer[0].b;
|
|
1449
|
+
var harnesse = [];
|
|
1450
|
+
treffer.forEach(function (t) { if (harnesse.indexOf(t.z.ziel.harness) === -1) harnesse.push(t.z.ziel.harness); });
|
|
1451
|
+
harnesse.sort(function (x, y) { return harnessName(x).localeCompare(harnessName(y), 'de'); });
|
|
1452
|
+
// Mitglieder über alle Harnesses ohne Doppelte; verlinkt zählt, was in jedem Harness liegt.
|
|
1453
|
+
var mitglieder = [];
|
|
1454
|
+
treffer.forEach(function (t) {
|
|
1455
|
+
bundleAlleMitglieder(t.b).forEach(function (n) { if (mitglieder.indexOf(n) === -1) mitglieder.push(n); });
|
|
1456
|
+
});
|
|
1457
|
+
var ueberall = mitglieder.filter(function (n) {
|
|
1458
|
+
return treffer.every(function (t) { return t.b.skills.indexOf(n) > -1; });
|
|
1459
|
+
});
|
|
1460
|
+
var gruppenZeilen = sortiert
|
|
1461
|
+
.filter(function (key) { return zeileInGruppe[key] === gk; })
|
|
1462
|
+
.map(function (key) { return alleZeile(zeilen[key], b.name); });
|
|
1463
|
+
// Nirgends verlinkte Mitglieder als blasse Zeile; „Wieder verlinken" in jedem Harness, in
|
|
1464
|
+
// dem das Mitglied fehlt.
|
|
1465
|
+
mitglieder
|
|
1466
|
+
.filter(function (n) { return treffer.every(function (t) { return t.b.skills.indexOf(n) === -1; }); })
|
|
1467
|
+
.sort(function (x, y) { return x.localeCompare(y, 'de'); })
|
|
1468
|
+
.forEach(function (n) {
|
|
1469
|
+
gruppenZeilen.push(nichtVerlinktZeile(n, bundleSkillNachName(b, n), function () {
|
|
1470
|
+
bundleAufnehmen(b, treffer
|
|
1471
|
+
.filter(function (t) { return (t.b.nichtVerlinkt || []).indexOf(n) > -1; })
|
|
1472
|
+
.map(function (t) { return t.z.ziel; }), [n]);
|
|
1473
|
+
}));
|
|
1474
|
+
});
|
|
1475
|
+
var verlinkteNamen = [];
|
|
1476
|
+
treffer.forEach(function (t) {
|
|
1477
|
+
t.b.skills.forEach(function (n) { if (verlinkteNamen.indexOf(n) === -1) verlinkteNamen.push(n); });
|
|
1478
|
+
});
|
|
1479
|
+
teil.appendChild(bundleRahmen(b, harnesse, ueberall.length, mitglieder.length,
|
|
1480
|
+
treffer.map(function (t) { return t.z.ziel; }), gruppenZeilen, function () {
|
|
1481
|
+
if (treffer.length === 1) frageBundleEntfernen(b, treffer[0].z.ziel);
|
|
1482
|
+
else frageBundleEntfernenAlle(b.name, verlinkteNamen, treffer);
|
|
1483
|
+
}, 'verz\u0000' + pfad + '\u0000' + gk));
|
|
1334
1484
|
});
|
|
1335
|
-
|
|
1485
|
+
|
|
1486
|
+
var ul = e('ul', 'linkliste');
|
|
1487
|
+
sortiert
|
|
1488
|
+
.filter(function (key) { return !zeileInGruppe[key]; })
|
|
1489
|
+
.forEach(function (key) { ul.appendChild(alleZeile(zeilen[key], null)); });
|
|
1490
|
+
if (ul.childNodes.length > 0) teil.appendChild(ul);
|
|
1336
1491
|
|
|
1337
1492
|
// MCP-Abschnitt (E61-GUI-Nachtrag): auf Kartenebene unter „Alle", wie unter dem Harness-Chip,
|
|
1338
1493
|
// der an diesem Pfad MCP kann.
|
|
@@ -1638,28 +1793,42 @@
|
|
|
1638
1793
|
// entfernenKontext.eintraege: [{harness, ort}], immer eines bei frageEntfernen (Einzel-Ziel),
|
|
1639
1794
|
// mehrere bei frageEntfernenAlle (E53 Pkt. 2, „Alle"-Zeile über mehrere Harnesses).
|
|
1640
1795
|
var entfernenKontext = null;
|
|
1641
|
-
/**
|
|
1796
|
+
/** Satz für den Entfernen-Dialog einer Zeile in einer Bundle-Gruppe (E114 L2): das Bundle bleibt. */
|
|
1797
|
+
function herausnehmenSatz(bundleName) {
|
|
1798
|
+
return ' Der Skill wird damit aus dem Bundle „' + bundleName + '“ herausgenommen: das Bundle bleibt, '
|
|
1799
|
+
+ 'das Aktualisieren legt den Link nicht wieder an. „Wieder verlinken“ oder „Vervollständigen“ am Bundle holt ihn zurück.';
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/** `bundleName` gesetzt heißt: der Link gehört zu einem Bundle, das Entfernen nimmt ihn dort
|
|
1803
|
+
heraus (E114, früher Zerbrechen nach E69). */
|
|
1642
1804
|
function frageEntfernen(name, ziel, bundleName) {
|
|
1643
1805
|
entfernenKontext = { art: 'link', name: name, eintraege: [{ harness: ziel.harness, ort: ziel.ort }] };
|
|
1644
1806
|
$('dx-titel').textContent = 'Link auf ' + name + ' entfernen?';
|
|
1645
1807
|
var text = 'Der Link in ' + zielTitel({ ziel: ziel }) + ' verschwindet. Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
|
|
1646
|
-
if (bundleName)
|
|
1647
|
-
text += ' Damit zerbricht das Bundle „' + bundleName + '“ hier: seine übrigen Links bleiben liegen, aber nicht mehr als Bundle.';
|
|
1648
|
-
}
|
|
1808
|
+
if (bundleName) text += herausnehmenSatz(bundleName);
|
|
1649
1809
|
$('dx-text').textContent = text;
|
|
1650
1810
|
zeige($('d-entfernen'));
|
|
1651
1811
|
}
|
|
1652
1812
|
|
|
1653
|
-
/**
|
|
1813
|
+
/** Zusatz für die Bundle-Dialoge (E114): was schon nicht mehr verlinkt ist, bleibt unberührt. */
|
|
1814
|
+
function nichtVerlinktSatz(namen) {
|
|
1815
|
+
return namen.length === 0 ? '' : ' Nicht verlinkt sind schon: ' + namen.join(', ') + '.';
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/** Entfernen des ganzen Bundles an einem Ziel (E69, E114 L2): alle noch verlinkten Mitglieder auf
|
|
1819
|
+
einmal, auch nach Teil-Löschung. */
|
|
1654
1820
|
function frageBundleEntfernen(b, ziel) {
|
|
1655
1821
|
entfernenKontext = { art: 'bundle', bundle: b.name, ziele: [ziel] };
|
|
1656
1822
|
$('dx-titel').textContent = 'Bundle ' + b.name + ' entfernen?';
|
|
1657
|
-
$('dx-text').textContent =
|
|
1658
|
-
|
|
1823
|
+
$('dx-text').textContent = (b.skills.length === 0
|
|
1824
|
+
? 'Kein Link mehr da, aus ' + zielTitel({ ziel: ziel }) + ' verschwindet nur der Bundle-Verweis.'
|
|
1825
|
+
: zahlwort(b.skills.length, 'Link', 'Links') + ' aus ' + zielTitel({ ziel: ziel })
|
|
1826
|
+
+ ' verschwinden: ' + b.skills.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.')
|
|
1827
|
+
+ nichtVerlinktSatz(b.nichtVerlinkt || []);
|
|
1659
1828
|
zeige($('d-entfernen'));
|
|
1660
1829
|
}
|
|
1661
|
-
/** Entfernen eines Bundles in allen Harnesses
|
|
1662
|
-
|
|
1830
|
+
/** Entfernen eines Bundles in allen Harnesses seiner Gruppe unter „Alle" (E95): `treffer` je
|
|
1831
|
+
Harness {b, z}, der Dialog nennt die Harnesses mit ihrer Linkzahl und die verlinkten Mitglieder. */
|
|
1663
1832
|
function frageBundleEntfernenAlle(name, mitglieder, treffer) {
|
|
1664
1833
|
entfernenKontext = { art: 'bundle', bundle: name, ziele: treffer.map(function (t) { return t.z.ziel; }) };
|
|
1665
1834
|
var proHarness = treffer.map(function (t) {
|
|
@@ -1667,13 +1836,15 @@
|
|
|
1667
1836
|
});
|
|
1668
1837
|
$('dx-titel').textContent = 'Bundle ' + name + ' entfernen?';
|
|
1669
1838
|
$('dx-text').textContent = 'Das Bundle verschwindet in ' + treffer.length + ' Harnesses (' + proHarness.join(', ')
|
|
1670
|
-
+ '): ' + mitglieder.join(', ') + '.
|
|
1839
|
+
+ ')' + (mitglieder.length === 0 ? '.' : ': ' + mitglieder.join(', ') + '.')
|
|
1840
|
+
+ ' Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
|
|
1671
1841
|
zeige($('d-entfernen'));
|
|
1672
1842
|
}
|
|
1673
1843
|
/** Entfernen einer „Alle"-Zeile (E53 Pkt. 2): entfernbareZiele sind die Ziele (voller z.ziel), in
|
|
1674
1844
|
denen der Link weg soll, nichtEntfernbareHarnesse die, in denen er nur über ein Plugin hängt
|
|
1675
|
-
und liegen bleibt. Der Dialog nennt beides.
|
|
1676
|
-
|
|
1845
|
+
und liegen bleibt. Der Dialog nennt beides. `bundleName` gesetzt: die Zeile steht in einer
|
|
1846
|
+
Bundle-Gruppe, der Skill wird dort herausgenommen (E114). */
|
|
1847
|
+
function frageEntfernenAlle(name, pfad, entfernbareZiele, nichtEntfernbareHarnesse, bundleName) {
|
|
1677
1848
|
entfernenKontext = {
|
|
1678
1849
|
art: 'link',
|
|
1679
1850
|
name: name,
|
|
@@ -1686,6 +1857,7 @@
|
|
|
1686
1857
|
text += ' In ' + nichtEntfernbareHarnesse.map(harnessName).join(', ') + ' bleibt er bestehen (nur über das Plugin).';
|
|
1687
1858
|
}
|
|
1688
1859
|
text += ' Der Checkout bleibt liegen, bis kein Link mehr darauf zeigt.';
|
|
1860
|
+
if (bundleName) text += herausnehmenSatz(bundleName);
|
|
1689
1861
|
$('dx-text').textContent = text;
|
|
1690
1862
|
zeige($('d-entfernen'));
|
|
1691
1863
|
}
|
|
@@ -2328,13 +2500,14 @@
|
|
|
2328
2500
|
var txt = e('span');
|
|
2329
2501
|
txt.appendChild(e('b', null, bundle.name));
|
|
2330
2502
|
txt.appendChild(e('span', 'notiz', bundle.beschreibung));
|
|
2331
|
-
var herkunft = e('span', 'mono', zahlwort(bundle.mitglieder, 'Skill', 'Skills')
|
|
2503
|
+
var herkunft = e('span', 'mono', zahlwort(bundle.mitglieder.length, 'Skill', 'Skills')
|
|
2332
2504
|
+ ' aus ' + zahlwort(bundle.quellen.length, 'Quelle', 'Quellen') + ' · ' + bundle.quelleName);
|
|
2333
2505
|
herkunft.title = bundle.quellen.join('\n');
|
|
2334
2506
|
txt.appendChild(herkunft);
|
|
2335
2507
|
if (ueberall) txt.appendChild(e('span', 'notiz', zieleZahl > 1 ? 'Hängt schon überall.' : 'Hängt hier schon.'));
|
|
2336
2508
|
else if (schon > 0) txt.appendChild(e('span', 'notiz', 'Hängt schon in ' + schon + ' von ' + zieleZahl + ' Harnesses.'));
|
|
2337
2509
|
label.appendChild(txt);
|
|
2510
|
+
bundleAufklappbereich(bundle, label, txt, i);
|
|
2338
2511
|
box.appendChild(label);
|
|
2339
2512
|
});
|
|
2340
2513
|
$('dsk-bundlebox').hidden = BUNDLES.length === 0 && BUNDLES_KAPUTT.length === 0;
|
|
@@ -2346,6 +2519,54 @@
|
|
|
2346
2519
|
}).join('\n');
|
|
2347
2520
|
}
|
|
2348
2521
|
|
|
2522
|
+
/** Aufklappbereich einer Bundle-Wahlzeile (E114 L1, F133=O3): die Skills des Bundles mit ihrer
|
|
2523
|
+
Beschreibung, vor der Installation. Muster und Verhalten wie skillAufklappbereich (E92): Knopf
|
|
2524
|
+
oben rechts am Label, Körper als eigenes Grid-Kind, preventDefault gegen das Umschalten der
|
|
2525
|
+
Checkbox, je Dialog-Öffnung zugeklappt. Name und Beschreibung kommen aus dem Skill-Katalog;
|
|
2526
|
+
liegt ein Mitglied in einer noch nicht eingerichteten Quelle, steht dort sein Pfad. */
|
|
2527
|
+
function bundleAufklappbereich(bundle, label, txt, index) {
|
|
2528
|
+
var koerperId = 'dsk-bundlekoerper-' + index;
|
|
2529
|
+
var knopf = e('button', 'aufklappknopf');
|
|
2530
|
+
knopf.type = 'button';
|
|
2531
|
+
knopf.setAttribute('aria-expanded', 'false');
|
|
2532
|
+
knopf.setAttribute('aria-controls', koerperId);
|
|
2533
|
+
knopf.setAttribute('aria-label', 'Skills des Bundles ' + bundle.name + ' ein- oder ausklappen');
|
|
2534
|
+
var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
2535
|
+
pfeil.setAttribute('viewBox', '0 0 16 16');
|
|
2536
|
+
pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
|
|
2537
|
+
pfeil.setAttribute('aria-hidden', 'true');
|
|
2538
|
+
pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
|
|
2539
|
+
knopf.appendChild(pfeil);
|
|
2540
|
+
|
|
2541
|
+
var koerper = e('div', 'wahlkoerper');
|
|
2542
|
+
koerper.id = koerperId;
|
|
2543
|
+
koerper.hidden = true;
|
|
2544
|
+
bundle.mitglieder.forEach(function (m) {
|
|
2545
|
+
var skill = bundleMitgliedSkill(m);
|
|
2546
|
+
var zeile = e('p', 'skilldetail bundlemitglied');
|
|
2547
|
+
if (skill) {
|
|
2548
|
+
zeile.appendChild(e('b', null, skill.name + ': '));
|
|
2549
|
+
zeile.appendChild(document.createTextNode(skill.beschreibung || 'keine Beschreibung in der SKILL.md'));
|
|
2550
|
+
} else {
|
|
2551
|
+
zeile.appendChild(e('b', null, m.pfad + ': '));
|
|
2552
|
+
zeile.appendChild(document.createTextNode('aus ' + m.quelleText + ', die Quelle ist hier noch nicht eingerichtet'));
|
|
2553
|
+
}
|
|
2554
|
+
koerper.appendChild(zeile);
|
|
2555
|
+
});
|
|
2556
|
+
|
|
2557
|
+
knopf.addEventListener('click', function (ev) {
|
|
2558
|
+
ev.preventDefault();
|
|
2559
|
+
var war = knopf.getAttribute('aria-expanded') === 'true';
|
|
2560
|
+
knopf.setAttribute('aria-expanded', war ? 'false' : 'true');
|
|
2561
|
+
koerper.hidden = war;
|
|
2562
|
+
});
|
|
2563
|
+
koerper.addEventListener('click', function (ev) { ev.preventDefault(); });
|
|
2564
|
+
|
|
2565
|
+
txt.classList.add('mitknopf');
|
|
2566
|
+
label.appendChild(knopf);
|
|
2567
|
+
label.appendChild(koerper);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2349
2570
|
/** Wie oft ein Bundle in den angesteuerten Zielen schon installiert ist (Name plus Quelle, E69). */
|
|
2350
2571
|
function bundleHaengtIn(teile, bundle) {
|
|
2351
2572
|
return teile.filter(function (z) {
|
|
@@ -798,6 +798,14 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
|
|
|
798
798
|
.bundlemitglieder{font-family:var(--mono);font-size:16px;letter-spacing:-.012em;color:var(--tinte-weich)}
|
|
799
799
|
.bundlekopf .knopf{margin-left:auto}
|
|
800
800
|
.bundlegruppe .linkliste li:last-child{border-bottom:1px solid var(--strich-zart)}
|
|
801
|
+
/* E114: Knöpfe und Aufklapp-Umschalter rechts im Kopf; der Kopf ist zugleich Klickfläche. */
|
|
802
|
+
.bundlekopf{cursor:pointer}
|
|
803
|
+
.bundleaktionen{margin-left:auto;display:inline-flex;align-items:center;gap:8px}
|
|
804
|
+
.bundleaktionen .knopf{margin-left:0}
|
|
805
|
+
.bundlegruppe .linkliste[hidden]{display:none}
|
|
806
|
+
/* Nicht verlinktes Mitglied (E114 L3): blass, der Knopf bleibt lesbar. */
|
|
807
|
+
.linkliste li.nichtverlinkt .name,.linkliste li.nichtverlinkt .was{color:var(--tinte-leise)}
|
|
808
|
+
.linkliste li.nichtverlinkt .quelle{color:var(--tinte-leise);font-style:italic}
|
|
801
809
|
|
|
802
810
|
/* Bundle- und Skill-Sektion im Dialog „Skills hinzufügen" (E69): zwei benannte Gruppen unter
|
|
803
811
|
demselben Suchfeld. */
|
|
@@ -659,7 +659,7 @@ export class Anwendungsfaelle {
|
|
|
659
659
|
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
660
660
|
entfernteLinks.push(name);
|
|
661
661
|
}
|
|
662
|
-
if (Anwendungsfaelle.
|
|
662
|
+
if (Anwendungsfaelle.bundleVerweiseFallenLassen(nachZustand, ziel))
|
|
663
663
|
bundlesGeaendert = true;
|
|
664
664
|
const konfigurationZurueckgebaut = kannZurueckbauen(harness) ? await harness.zurueckbauen(ziel.ort) : false;
|
|
665
665
|
zielErgebnisse.push({ ziel, entfernteLinks, konfigurationZurueckgebaut });
|
|
@@ -801,12 +801,12 @@ export class Anwendungsfaelle {
|
|
|
801
801
|
throw new QuelleAufraeumenLokaleAenderungen(inventar.checkoutsMitLokalenAenderungen);
|
|
802
802
|
for (const link of inventar.links)
|
|
803
803
|
await this.p.ziele.linkEntfernen(link.ziel, link.verzeichnis, link.name);
|
|
804
|
-
// Wo dabei ein Bundle-Mitglied fiel,
|
|
805
|
-
//
|
|
804
|
+
// Wo dabei ein Bundle-Mitglied fiel, gilt es als herausgenommen (E114), wie beim einzelnen
|
|
805
|
+
// `linkEntfernen`: das Bundle bleibt, der Abgleich legt das Mitglied nicht wieder an.
|
|
806
806
|
const zustand = await this.p.zustand.lesen();
|
|
807
807
|
let bundlesGeaendert = false;
|
|
808
808
|
for (const link of inventar.links)
|
|
809
|
-
if (Anwendungsfaelle.
|
|
809
|
+
if (Anwendungsfaelle.bundleMitgliederHerausnehmen(zustand, link.ziel, [link.name]).length > 0)
|
|
810
810
|
bundlesGeaendert = true;
|
|
811
811
|
if (bundlesGeaendert)
|
|
812
812
|
await this.p.zustand.schreiben(zustand);
|
|
@@ -1122,8 +1122,11 @@ export class Anwendungsfaelle {
|
|
|
1122
1122
|
}
|
|
1123
1123
|
return angelegt;
|
|
1124
1124
|
}
|
|
1125
|
-
/**
|
|
1126
|
-
|
|
1125
|
+
/**
|
|
1126
|
+
* Die Zugehörigkeit der Links eines Bundles im Ziel merken (E69), einen alten Eintrag ersetzend.
|
|
1127
|
+
* `herausgenommen` (E114) wird nur geschrieben, wenn es etwas enthält.
|
|
1128
|
+
*/
|
|
1129
|
+
async bundleInstallationMerken(ziel, bundle, skills, herausgenommen = []) {
|
|
1127
1130
|
const zustand = await this.p.zustand.lesen();
|
|
1128
1131
|
const schluessel = zielSchluessel(ziel);
|
|
1129
1132
|
const eigener = bundleSchluessel(bundle);
|
|
@@ -1135,27 +1138,43 @@ export class Anwendungsfaelle {
|
|
|
1135
1138
|
bundle: bundle.name,
|
|
1136
1139
|
quelle: bundle.quelle,
|
|
1137
1140
|
skills,
|
|
1141
|
+
...(herausgenommen.length === 0 ? {} : { herausgenommen }),
|
|
1138
1142
|
});
|
|
1139
1143
|
await this.p.zustand.schreiben(zustand);
|
|
1140
1144
|
}
|
|
1141
1145
|
/**
|
|
1142
|
-
* Zerbrechen
|
|
1143
|
-
* Bundle
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
+
* Herausnehmen (E114 L2, ersetzt das Zerbrechen aus E69): Wird ein einzelner Link entfernt, der
|
|
1147
|
+
* zu einem Bundle gehört, bleibt das Bundle im Ziel und merkt sich das Mitglied als herausgenommen
|
|
1148
|
+
* — auch wenn danach keines mehr verlinkt ist („0 von N", weiter entfernbar). Ändert nur den
|
|
1149
|
+
* übergebenen Zustand, geschrieben wird er vom Aufrufer. Gibt die Namen der betroffenen Bundles
|
|
1150
|
+
* zurück, damit die Oberfläche es sagen kann.
|
|
1146
1151
|
*/
|
|
1147
|
-
static
|
|
1152
|
+
static bundleMitgliederHerausnehmen(zustand, ziel, namen) {
|
|
1148
1153
|
const schluessel = zielSchluessel(ziel);
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1154
|
+
const betroffen = [];
|
|
1155
|
+
for (const installation of zustand.bundleInstallationen) {
|
|
1151
1156
|
if (zielSchluessel(Anwendungsfaelle.installationZiel(installation)) !== schluessel)
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1157
|
+
continue;
|
|
1158
|
+
const raus = installation.skills.filter((name) => namen.includes(name));
|
|
1159
|
+
if (raus.length === 0)
|
|
1160
|
+
continue;
|
|
1161
|
+
installation.skills = installation.skills.filter((name) => !raus.includes(name));
|
|
1162
|
+
installation.herausgenommen = [...(installation.herausgenommen ?? []), ...raus];
|
|
1163
|
+
betroffen.push(installation.bundle);
|
|
1164
|
+
}
|
|
1165
|
+
return betroffen;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Bundle-Verweise eines Ziels fallen lassen, wenn „Säubern" dort alle Links abbaut (E69). Kein
|
|
1169
|
+
* Herausnehmen: Säubern räumt den Harness ab, ein Bundle mit „0 von N" bliebe ein Rest, den der
|
|
1170
|
+
* Nutzer gerade loswerden wollte. Darum fallen alle Verweise des Ziels, auch die von Bundles, von
|
|
1171
|
+
* denen gerade kein Link mehr lag.
|
|
1172
|
+
*/
|
|
1173
|
+
static bundleVerweiseFallenLassen(zustand, ziel) {
|
|
1174
|
+
const schluessel = zielSchluessel(ziel);
|
|
1175
|
+
const vorher = zustand.bundleInstallationen.length;
|
|
1176
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((installation) => zielSchluessel(Anwendungsfaelle.installationZiel(installation)) !== schluessel);
|
|
1177
|
+
return zustand.bundleInstallationen.length !== vorher;
|
|
1159
1178
|
}
|
|
1160
1179
|
/**
|
|
1161
1180
|
* Bundle installieren (E69): das Bundle als Ganzes in ein Ziel. Erst wird alles geprüft — jedes
|
|
@@ -1205,9 +1224,11 @@ export class Anwendungsfaelle {
|
|
|
1205
1224
|
return treffer[0];
|
|
1206
1225
|
}
|
|
1207
1226
|
/**
|
|
1208
|
-
* Bundle entfernen (E69): alle seine
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1227
|
+
* Bundle entfernen (E69): alle seine noch verlinkten Mitglieder im Ziel und den Bundle-Verweis,
|
|
1228
|
+
* auch nach Teil-Löschung (E114 L2): herausgenommene stehen nicht in `skills` und bleiben
|
|
1229
|
+
* unberührt, ein Link gleichen Namens an ihrer Stelle gehört nicht dem Bundle. Entfernt wird
|
|
1230
|
+
* direkt über den Ziele-Port, nicht über den Anwendungsfall `linkEntfernen` — der macht
|
|
1231
|
+
* Zeiger-Arbeit (E57) und nähme jedes Mitglied einzeln heraus.
|
|
1211
1232
|
*/
|
|
1212
1233
|
async bundleEntfernen(args) {
|
|
1213
1234
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
@@ -1237,6 +1258,11 @@ export class Anwendungsfaelle {
|
|
|
1237
1258
|
* Wirkt auf das Bundle als Ganzes: Lässt sich eine Definitionsänderung nicht vollständig nachziehen
|
|
1238
1259
|
* (Quelle weg, Pfad kein Skill mehr, Namenskonflikt), bleibt das Bundle im betroffenen Ziel auf dem
|
|
1239
1260
|
* alten Stand und der Lauf meldet es, statt es halb nachzuziehen (F66b).
|
|
1261
|
+
*
|
|
1262
|
+
* Herausgenommene Mitglieder (E114 L5) lässt der Abgleich aus: kein Link, keine Konfliktprüfung
|
|
1263
|
+
* (ein anderer Link gleichen Namens an ihrer Stelle ist dann kein Konflikt). Fällt eines aus der
|
|
1264
|
+
* Definition, fällt es auch aus der Liste. `aufnehmen` holt herausgenommene zurück: 'alle' ist
|
|
1265
|
+
* „Vervollständigen" (L4), eine Namensliste „Wieder verlinken" einzelner Mitglieder (L3).
|
|
1240
1266
|
*/
|
|
1241
1267
|
async bundleAbgleich(args) {
|
|
1242
1268
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -1256,7 +1282,10 @@ export class Anwendungsfaelle {
|
|
|
1256
1282
|
// ein Fehlschlag (kein Netz) arbeitet mit dem vorhandenen Stand weiter.
|
|
1257
1283
|
await this.refsNachziehen(installation.quelle);
|
|
1258
1284
|
const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
|
|
1259
|
-
const { funde, neue } = await this.bundleAufloesen(bundle);
|
|
1285
|
+
const { funde: alle, neue } = await this.bundleAufloesen(bundle);
|
|
1286
|
+
const aufnehmen = args?.aufnehmen;
|
|
1287
|
+
const herausgenommen = (installation.herausgenommen ?? []).filter((name) => aufnehmen !== 'alle' && !(aufnehmen ?? []).includes(name) && alle.some((f) => f.skill.name === name));
|
|
1288
|
+
const funde = alle.filter((f) => !herausgenommen.includes(f.skill.name));
|
|
1260
1289
|
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
1261
1290
|
const { konflikte } = await this.bundleKonflikte(bundle, ziel, verzeichnis, funde);
|
|
1262
1291
|
if (konflikte.length > 0)
|
|
@@ -1272,7 +1301,7 @@ export class Anwendungsfaelle {
|
|
|
1272
1301
|
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
1273
1302
|
entfernt.push(name);
|
|
1274
1303
|
}
|
|
1275
|
-
await this.bundleInstallationMerken(ziel, bundle, namen);
|
|
1304
|
+
await this.bundleInstallationMerken(ziel, bundle, namen, herausgenommen);
|
|
1276
1305
|
const gelegt = namen.filter((n) => !installation.skills.includes(n));
|
|
1277
1306
|
ergebnis.push({
|
|
1278
1307
|
...kopf,
|
|
@@ -1486,7 +1515,8 @@ export class Anwendungsfaelle {
|
|
|
1486
1515
|
const version = args.version;
|
|
1487
1516
|
if (version === undefined) {
|
|
1488
1517
|
const fund = await this.skillFinden(args.skill, args.quelle);
|
|
1489
|
-
|
|
1518
|
+
const ergebnis = await this.linkAusFund(harness, args.ziel, fund, args.ersetzen, args.lokal);
|
|
1519
|
+
return Anwendungsfaelle.hinweisAnhaengen(ergebnis, await this.bundleMitgliedZurueckholen(args.ziel, fund));
|
|
1490
1520
|
}
|
|
1491
1521
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
1492
1522
|
throw new EinschraenkungOhneZeiger(args.ziel, version);
|
|
@@ -1500,7 +1530,57 @@ export class Anwendungsfaelle {
|
|
|
1500
1530
|
const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
|
|
1501
1531
|
await this.zeigerErgaenzen(zeigerPfad, fund, version);
|
|
1502
1532
|
const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
|
|
1503
|
-
return hinweis === undefined ? link : { ...link, hinweis };
|
|
1533
|
+
return Anwendungsfaelle.hinweisAnhaengen(hinweis === undefined ? link : { ...link, hinweis }, await this.bundleMitgliedZurueckholen(args.ziel, fund));
|
|
1534
|
+
}
|
|
1535
|
+
/** Weitere Hinweiszeilen an ein Link-Ergebnis hängen, ohne einen vorhandenen Hinweis zu verdrängen. */
|
|
1536
|
+
static hinweisAnhaengen(ergebnis, zeilen) {
|
|
1537
|
+
if (zeilen.length === 0)
|
|
1538
|
+
return ergebnis;
|
|
1539
|
+
const alle = ergebnis.hinweis === undefined ? zeilen : [ergebnis.hinweis, ...zeilen];
|
|
1540
|
+
return { ...ergebnis, hinweis: alle.join('\n') };
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* `link` eines herausgenommenen Bundle-Mitglieds (E114 L6): Ist es derselbe Skill aus derselben
|
|
1544
|
+
* Quelle, wie ihn die Definition nennt (Quelle und Pfad, dieselbe Probe wie die Übernahme in
|
|
1545
|
+
* F67), gehört er wieder zum Bundle. Die Definition wird nur gelesen, wenn der Name in diesem
|
|
1546
|
+
* Ziel herausgenommen ist; lässt sie sich nicht lesen, bleibt es beim Einzel-Link. Gibt je
|
|
1547
|
+
* zurückgeholtem Bundle eine Hinweiszeile zurück.
|
|
1548
|
+
*/
|
|
1549
|
+
async bundleMitgliedZurueckholen(ziel, fund) {
|
|
1550
|
+
const name = fund.skill.name;
|
|
1551
|
+
const kandidaten = Anwendungsfaelle.installationenAn(await this.p.zustand.lesen(), ziel).filter((i) => (i.herausgenommen ?? []).includes(name));
|
|
1552
|
+
if (kandidaten.length === 0)
|
|
1553
|
+
return [];
|
|
1554
|
+
const passend = [];
|
|
1555
|
+
for (const installation of kandidaten) {
|
|
1556
|
+
try {
|
|
1557
|
+
const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
|
|
1558
|
+
const gleich = bundle.mitglieder.some((m) => quelleSchluessel(m.quelle) === quelleSchluessel(fund.quelle) && m.pfad === fund.skill.pfad);
|
|
1559
|
+
if (gleich)
|
|
1560
|
+
passend.push(bundleSchluessel(bundle));
|
|
1561
|
+
}
|
|
1562
|
+
catch {
|
|
1563
|
+
// Definition nicht lesbar: der Link bleibt ein Einzel-Link, das Mitglied herausgenommen.
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (passend.length === 0)
|
|
1567
|
+
return [];
|
|
1568
|
+
const zustand = await this.p.zustand.lesen();
|
|
1569
|
+
const zeilen = [];
|
|
1570
|
+
for (const installation of Anwendungsfaelle.installationenAn(zustand, ziel)) {
|
|
1571
|
+
if (!passend.includes(bundleSchluessel({ name: installation.bundle, quelle: installation.quelle })))
|
|
1572
|
+
continue;
|
|
1573
|
+
const rest = (installation.herausgenommen ?? []).filter((n) => n !== name);
|
|
1574
|
+
if (rest.length === 0)
|
|
1575
|
+
delete installation.herausgenommen;
|
|
1576
|
+
else
|
|
1577
|
+
installation.herausgenommen = rest;
|
|
1578
|
+
if (!installation.skills.includes(name))
|
|
1579
|
+
installation.skills.push(name);
|
|
1580
|
+
zeilen.push(`„${name}" gehört wieder zum Bundle „${installation.bundle}".`);
|
|
1581
|
+
}
|
|
1582
|
+
await this.p.zustand.schreiben(zustand);
|
|
1583
|
+
return zeilen;
|
|
1504
1584
|
}
|
|
1505
1585
|
/** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
|
|
1506
1586
|
async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
|
|
@@ -1553,9 +1633,9 @@ export class Anwendungsfaelle {
|
|
|
1553
1633
|
* Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
|
|
1554
1634
|
* einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
|
|
1555
1635
|
*
|
|
1556
|
-
* Gehörte der Link zu einem Bundle,
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1636
|
+
* Gehörte der Link zu einem Bundle, bleibt das Bundle im Ziel und merkt sich den Skill als
|
|
1637
|
+
* herausgenommen (E114 L2). `herausgenommenAus` nennt die Bundles, damit die Oberfläche es sagen
|
|
1638
|
+
* kann.
|
|
1559
1639
|
*/
|
|
1560
1640
|
async linkEntfernen(args) {
|
|
1561
1641
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
@@ -1563,25 +1643,25 @@ export class Anwendungsfaelle {
|
|
|
1563
1643
|
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
1564
1644
|
await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
|
|
1565
1645
|
const zustand = await this.p.zustand.lesen();
|
|
1566
|
-
const
|
|
1567
|
-
if (
|
|
1646
|
+
const herausgenommenAus = Anwendungsfaelle.bundleMitgliederHerausnehmen(zustand, args.ziel, [args.skill]);
|
|
1647
|
+
if (herausgenommenAus.length > 0)
|
|
1568
1648
|
await this.p.zustand.schreiben(zustand);
|
|
1569
1649
|
if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
|
|
1570
1650
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
1571
1651
|
if (zeigerPfad !== undefined)
|
|
1572
|
-
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).',
|
|
1573
|
-
return {
|
|
1652
|
+
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).', herausgenommenAus };
|
|
1653
|
+
return { herausgenommenAus };
|
|
1574
1654
|
}
|
|
1575
1655
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
1576
|
-
return {
|
|
1656
|
+
return { herausgenommenAus };
|
|
1577
1657
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
1578
1658
|
if (zeigerPfad === undefined)
|
|
1579
|
-
return {
|
|
1659
|
+
return { herausgenommenAus };
|
|
1580
1660
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
1581
1661
|
if (!zeiger.skills.delete(args.skill))
|
|
1582
|
-
return {
|
|
1662
|
+
return { herausgenommenAus };
|
|
1583
1663
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
1584
|
-
return {
|
|
1664
|
+
return { herausgenommenAus };
|
|
1585
1665
|
}
|
|
1586
1666
|
/**
|
|
1587
1667
|
* Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
|
|
@@ -2580,14 +2660,20 @@ export class Anwendungsfaelle {
|
|
|
2580
2660
|
for (const ei of eintraege)
|
|
2581
2661
|
if (ei.art === 'fremd')
|
|
2582
2662
|
uebernahme.set(ei.name, await this.erkennenFremd(harness, verzeichnis, ei, bekannt, true));
|
|
2583
|
-
// Bundles dieses Ziels (E69), rein aus dem Zustand:
|
|
2584
|
-
// wirklich noch liegen
|
|
2663
|
+
// Bundles dieses Ziels (E69), rein aus dem Zustand: verlinkt sind die Mitglieder, die dort
|
|
2664
|
+
// auch wirklich noch liegen; herausgenommene und von Hand gelöschte stehen als nicht
|
|
2665
|
+
// verlinkt daneben (E114 L3), die Gruppe zeigt alle.
|
|
2585
2666
|
const eigeneNamen = new Set(eintraege.filter((ei) => ei.art !== 'fremd').map((ei) => (ei.art === 'link' ? ei.link.name : ei.name)));
|
|
2586
|
-
const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) =>
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2667
|
+
const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) => {
|
|
2668
|
+
const herausgenommen = i.herausgenommen ?? [];
|
|
2669
|
+
return {
|
|
2670
|
+
name: i.bundle,
|
|
2671
|
+
quelle: i.quelle,
|
|
2672
|
+
skills: i.skills.filter((name) => eigeneNamen.has(name)),
|
|
2673
|
+
nichtVerlinkt: [...i.skills.filter((name) => !eigeneNamen.has(name)), ...herausgenommen],
|
|
2674
|
+
herausgenommen,
|
|
2675
|
+
};
|
|
2676
|
+
});
|
|
2591
2677
|
ergebnis.push({
|
|
2592
2678
|
ziel,
|
|
2593
2679
|
verzeichnis,
|