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/adapter/git.js +215 -10
- package/dist/adapter/sops.js +107 -0
- package/dist/adapter/zustand-json.js +2 -1
- package/dist/aenderungen.json +6 -0
- package/dist/cli/hilfe.js +44 -10
- package/dist/cli/main.js +67 -13
- package/dist/cli/verlauf-anzeige.js +17 -0
- package/dist/gui/main.js +80 -15
- package/dist/gui/preload.cjs +2 -0
- package/dist/gui/renderer/renderer.js +325 -88
- package/dist/gui/renderer/styles.css +8 -0
- package/dist/kern/anwendungsfaelle.js +250 -62
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -1358,19 +1387,101 @@ export class Anwendungsfaelle {
|
|
|
1358
1387
|
* dieselbe Weiche wie bei `verwaisteAufraeumen`.
|
|
1359
1388
|
*/
|
|
1360
1389
|
async doctor(args) {
|
|
1361
|
-
const
|
|
1390
|
+
const ergebnis = await this.doctorPruefen(args?.rueckfragen ?? true);
|
|
1391
|
+
// Nur die SOPS-Befunde kommen in den Verlauf (E115); fehlende Arbeitsverzeichnisse meldet der
|
|
1392
|
+
// alleinstehende doctor wie bisher nur auf der Konsole.
|
|
1393
|
+
await this.sopsVermerken(args?.ausloeser ?? 'cli', ergebnis.sops, true);
|
|
1394
|
+
return ergebnis;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* doctor ohne Verlauf, damit der Aktualisieren-Lauf die Befunde in seinen eigenen Eintrag nimmt.
|
|
1398
|
+
* Dazu die SOPS-Prüfung (E115): Checkouts geführter Quellen, deren SOPS-Datei noch verschlüsselt
|
|
1399
|
+
* im Arbeitsbaum liegt, schreibt der Quellen-Port neu, wo die Probe klappt. Sie ist keine
|
|
1400
|
+
* Rückfrage wert: die Datei entspricht dem Commit, nur entschlüsselt.
|
|
1401
|
+
*/
|
|
1402
|
+
async doctorPruefen(rueckfragen) {
|
|
1362
1403
|
const zustand = await this.p.zustand.lesen();
|
|
1363
1404
|
const fehlend = [];
|
|
1364
1405
|
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
1365
1406
|
if (!(await this.p.zeiger.verzeichnisExistiert(pfad)))
|
|
1366
1407
|
fehlend.push(pfad);
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1408
|
+
let vergessen = [];
|
|
1409
|
+
if (fehlend.length > 0 && rueckfragen && (await this.p.rueckfrage.bestaetigen(doctorFrage(fehlend)))) {
|
|
1410
|
+
for (const pfad of fehlend)
|
|
1411
|
+
await this.arbeitsverzeichnisVergessen(pfad);
|
|
1412
|
+
vergessen = [...fehlend];
|
|
1413
|
+
}
|
|
1414
|
+
const sops = [];
|
|
1415
|
+
for (const checkout of await this.gefuehrteCheckouts(zustand.quellen)) {
|
|
1416
|
+
const reparaturen = await this.p.quellen.sopsReparieren(checkout).catch(() => []);
|
|
1417
|
+
for (const reparatur of reparaturen)
|
|
1418
|
+
sops.push({ checkout, ...reparatur });
|
|
1419
|
+
}
|
|
1420
|
+
return { fehlendeArbeitsverzeichnisse: fehlend, vergessen, sops };
|
|
1421
|
+
}
|
|
1422
|
+
/** Checkouts, deren Quelle der Zustand führt (E54), mit dem Anzeigenamen der geführten Quelle. */
|
|
1423
|
+
async gefuehrteCheckouts(quellen) {
|
|
1424
|
+
const bekannt = new Map(quellen.map((q) => [quelleSchluessel(q), q]));
|
|
1425
|
+
const ergebnis = [];
|
|
1426
|
+
for (const checkout of await this.p.quellen.checkouts()) {
|
|
1427
|
+
const gefuehrt = bekannt.get(quelleSchluessel(checkout.quelle));
|
|
1428
|
+
if (gefuehrt !== undefined)
|
|
1429
|
+
ergebnis.push({ ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) });
|
|
1430
|
+
}
|
|
1431
|
+
return ergebnis;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* SOPS-Dateien, die im Arbeitsbaum eines Checkouts noch verschlüsselt liegen (E115), für
|
|
1435
|
+
* `hmgr status`. Liest nur, ruft kein sops auf und repariert nichts.
|
|
1436
|
+
*/
|
|
1437
|
+
async sopsVerschluesselteDateien() {
|
|
1438
|
+
const ergebnis = [];
|
|
1439
|
+
for (const checkout of await this.gefuehrteCheckouts((await this.p.zustand.lesen()).quellen)) {
|
|
1440
|
+
const dateien = await this.p.quellen.sopsDateien(checkout).catch(() => []);
|
|
1441
|
+
for (const d of dateien)
|
|
1442
|
+
if (d.verschluesselt)
|
|
1443
|
+
ergebnis.push({ checkout, datei: d.datei });
|
|
1444
|
+
}
|
|
1445
|
+
return ergebnis;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* SOPS-Befunde als Verlaufsereignisse (E115). „Verschlüsselt" und ein gescheitertes Neuschreiben
|
|
1449
|
+
* kommen einmal je Datei und Grund (`gemerkt.sops`), „repariert" jedes Mal, wenn es geschieht.
|
|
1450
|
+
* `vollstaendig`: die Befunde decken alle Checkouts ab, dann ersetzen sie die gemerkte Liste;
|
|
1451
|
+
* sonst (ein einzelner Checkout beim Verlinken) kommen sie dazu.
|
|
1452
|
+
*/
|
|
1453
|
+
async sopsEreignisse(befunde, vollstaendig) {
|
|
1454
|
+
const bisher = (await this.p.verlauf.lesen()).gemerkt.sops ?? [];
|
|
1455
|
+
const aktuell = [];
|
|
1456
|
+
const ereignisse = [];
|
|
1457
|
+
for (const befund of befunde) {
|
|
1458
|
+
const quelle = quelleAlsText(befund.checkout.quelle);
|
|
1459
|
+
const { ref } = befund.checkout;
|
|
1460
|
+
if (befund.ergebnis === 'repariert') {
|
|
1461
|
+
ereignisse.push({ art: 'sops-repariert', quelle, ref, datei: befund.datei });
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const art = befund.ergebnis === 'verschluesselt' ? befund.grund : 'fehlgeschlagen';
|
|
1465
|
+
const schluessel = `${quelle} @ ${ref}: ${befund.datei} | ${art}`;
|
|
1466
|
+
if (aktuell.includes(schluessel))
|
|
1467
|
+
continue;
|
|
1468
|
+
aktuell.push(schluessel);
|
|
1469
|
+
if (bisher.includes(schluessel))
|
|
1470
|
+
continue;
|
|
1471
|
+
if (befund.ergebnis === 'verschluesselt')
|
|
1472
|
+
ereignisse.push({ art: 'sops-verschluesselt', quelle, ref, datei: befund.datei, grund: befund.grund });
|
|
1473
|
+
else
|
|
1474
|
+
ereignisse.push({
|
|
1475
|
+
art: 'fehler',
|
|
1476
|
+
betrifft: `${quelle} @ ${ref}: ${befund.datei}`,
|
|
1477
|
+
fehler: `verschlüsselte Datei nicht neu geschrieben: ${befund.fehler}`,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return { ereignisse, gemerkt: vollstaendig ? aktuell : [...new Set([...bisher, ...aktuell])] };
|
|
1481
|
+
}
|
|
1482
|
+
async sopsVermerken(ausloeser, befunde, vollstaendig) {
|
|
1483
|
+
const { ereignisse, gemerkt } = await this.sopsEreignisse(befunde, vollstaendig);
|
|
1484
|
+
await this.verlaufVermerken(ausloeser, ereignisse, gemerkt);
|
|
1374
1485
|
}
|
|
1375
1486
|
/**
|
|
1376
1487
|
* Mehrere composer.json unter einem Verzeichnis (E26): hmgr rät nicht, es fragt. Bleibt die Frage
|
|
@@ -1451,7 +1562,11 @@ export class Anwendungsfaelle {
|
|
|
1451
1562
|
throw new OrtNichtUnterstuetzt(harness.name, ziel.ort.art);
|
|
1452
1563
|
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
1453
1564
|
// Die Prüfung auf einen fremden Eintrag macht der Ziele-Port unmittelbar vor dem Anlegen (E12).
|
|
1454
|
-
|
|
1565
|
+
// Nur Quelle, Ref und Pfad gehen in den Link, die SOPS-Befunde nicht.
|
|
1566
|
+
const { sopsVerschluesselt, ...checkout } = await this.p.quellen.checkout(fund.quelle, fund.ref, [fund.skill]);
|
|
1567
|
+
// Verschlüsselt angekommene SOPS-Dateien (E115) gleich melden, nicht erst beim nächsten Lauf.
|
|
1568
|
+
if (sopsVerschluesselt.length > 0)
|
|
1569
|
+
await this.sopsVermerken('aktion', sopsVerschluesselt.map((v) => ({ checkout, datei: v.datei, ergebnis: 'verschluesselt', grund: v.grund })), false);
|
|
1455
1570
|
await harness.vorbereiten(ziel.ort);
|
|
1456
1571
|
await this.p.ziele.linkLegen(ziel, verzeichnis, fund.skill.name, checkout, fund.skill, ersetzen);
|
|
1457
1572
|
return { ziel, name: fund.skill.name, checkout, skill: fund.skill };
|
|
@@ -1486,7 +1601,8 @@ export class Anwendungsfaelle {
|
|
|
1486
1601
|
const version = args.version;
|
|
1487
1602
|
if (version === undefined) {
|
|
1488
1603
|
const fund = await this.skillFinden(args.skill, args.quelle);
|
|
1489
|
-
|
|
1604
|
+
const ergebnis = await this.linkAusFund(harness, args.ziel, fund, args.ersetzen, args.lokal);
|
|
1605
|
+
return Anwendungsfaelle.hinweisAnhaengen(ergebnis, await this.bundleMitgliedZurueckholen(args.ziel, fund));
|
|
1490
1606
|
}
|
|
1491
1607
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
1492
1608
|
throw new EinschraenkungOhneZeiger(args.ziel, version);
|
|
@@ -1500,7 +1616,57 @@ export class Anwendungsfaelle {
|
|
|
1500
1616
|
const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
|
|
1501
1617
|
await this.zeigerErgaenzen(zeigerPfad, fund, version);
|
|
1502
1618
|
const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
|
|
1503
|
-
return hinweis === undefined ? link : { ...link, hinweis };
|
|
1619
|
+
return Anwendungsfaelle.hinweisAnhaengen(hinweis === undefined ? link : { ...link, hinweis }, await this.bundleMitgliedZurueckholen(args.ziel, fund));
|
|
1620
|
+
}
|
|
1621
|
+
/** Weitere Hinweiszeilen an ein Link-Ergebnis hängen, ohne einen vorhandenen Hinweis zu verdrängen. */
|
|
1622
|
+
static hinweisAnhaengen(ergebnis, zeilen) {
|
|
1623
|
+
if (zeilen.length === 0)
|
|
1624
|
+
return ergebnis;
|
|
1625
|
+
const alle = ergebnis.hinweis === undefined ? zeilen : [ergebnis.hinweis, ...zeilen];
|
|
1626
|
+
return { ...ergebnis, hinweis: alle.join('\n') };
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* `link` eines herausgenommenen Bundle-Mitglieds (E114 L6): Ist es derselbe Skill aus derselben
|
|
1630
|
+
* Quelle, wie ihn die Definition nennt (Quelle und Pfad, dieselbe Probe wie die Übernahme in
|
|
1631
|
+
* F67), gehört er wieder zum Bundle. Die Definition wird nur gelesen, wenn der Name in diesem
|
|
1632
|
+
* Ziel herausgenommen ist; lässt sie sich nicht lesen, bleibt es beim Einzel-Link. Gibt je
|
|
1633
|
+
* zurückgeholtem Bundle eine Hinweiszeile zurück.
|
|
1634
|
+
*/
|
|
1635
|
+
async bundleMitgliedZurueckholen(ziel, fund) {
|
|
1636
|
+
const name = fund.skill.name;
|
|
1637
|
+
const kandidaten = Anwendungsfaelle.installationenAn(await this.p.zustand.lesen(), ziel).filter((i) => (i.herausgenommen ?? []).includes(name));
|
|
1638
|
+
if (kandidaten.length === 0)
|
|
1639
|
+
return [];
|
|
1640
|
+
const passend = [];
|
|
1641
|
+
for (const installation of kandidaten) {
|
|
1642
|
+
try {
|
|
1643
|
+
const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
|
|
1644
|
+
const gleich = bundle.mitglieder.some((m) => quelleSchluessel(m.quelle) === quelleSchluessel(fund.quelle) && m.pfad === fund.skill.pfad);
|
|
1645
|
+
if (gleich)
|
|
1646
|
+
passend.push(bundleSchluessel(bundle));
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
// Definition nicht lesbar: der Link bleibt ein Einzel-Link, das Mitglied herausgenommen.
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (passend.length === 0)
|
|
1653
|
+
return [];
|
|
1654
|
+
const zustand = await this.p.zustand.lesen();
|
|
1655
|
+
const zeilen = [];
|
|
1656
|
+
for (const installation of Anwendungsfaelle.installationenAn(zustand, ziel)) {
|
|
1657
|
+
if (!passend.includes(bundleSchluessel({ name: installation.bundle, quelle: installation.quelle })))
|
|
1658
|
+
continue;
|
|
1659
|
+
const rest = (installation.herausgenommen ?? []).filter((n) => n !== name);
|
|
1660
|
+
if (rest.length === 0)
|
|
1661
|
+
delete installation.herausgenommen;
|
|
1662
|
+
else
|
|
1663
|
+
installation.herausgenommen = rest;
|
|
1664
|
+
if (!installation.skills.includes(name))
|
|
1665
|
+
installation.skills.push(name);
|
|
1666
|
+
zeilen.push(`„${name}" gehört wieder zum Bundle „${installation.bundle}".`);
|
|
1667
|
+
}
|
|
1668
|
+
await this.p.zustand.schreiben(zustand);
|
|
1669
|
+
return zeilen;
|
|
1504
1670
|
}
|
|
1505
1671
|
/** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
|
|
1506
1672
|
async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
|
|
@@ -1553,9 +1719,9 @@ export class Anwendungsfaelle {
|
|
|
1553
1719
|
* Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
|
|
1554
1720
|
* einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
|
|
1555
1721
|
*
|
|
1556
|
-
* Gehörte der Link zu einem Bundle,
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1722
|
+
* Gehörte der Link zu einem Bundle, bleibt das Bundle im Ziel und merkt sich den Skill als
|
|
1723
|
+
* herausgenommen (E114 L2). `herausgenommenAus` nennt die Bundles, damit die Oberfläche es sagen
|
|
1724
|
+
* kann.
|
|
1559
1725
|
*/
|
|
1560
1726
|
async linkEntfernen(args) {
|
|
1561
1727
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
@@ -1563,25 +1729,25 @@ export class Anwendungsfaelle {
|
|
|
1563
1729
|
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
1564
1730
|
await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
|
|
1565
1731
|
const zustand = await this.p.zustand.lesen();
|
|
1566
|
-
const
|
|
1567
|
-
if (
|
|
1732
|
+
const herausgenommenAus = Anwendungsfaelle.bundleMitgliederHerausnehmen(zustand, args.ziel, [args.skill]);
|
|
1733
|
+
if (herausgenommenAus.length > 0)
|
|
1568
1734
|
await this.p.zustand.schreiben(zustand);
|
|
1569
1735
|
if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
|
|
1570
1736
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
1571
1737
|
if (zeigerPfad !== undefined)
|
|
1572
|
-
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).',
|
|
1573
|
-
return {
|
|
1738
|
+
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).', herausgenommenAus };
|
|
1739
|
+
return { herausgenommenAus };
|
|
1574
1740
|
}
|
|
1575
1741
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
1576
|
-
return {
|
|
1742
|
+
return { herausgenommenAus };
|
|
1577
1743
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
1578
1744
|
if (zeigerPfad === undefined)
|
|
1579
|
-
return {
|
|
1745
|
+
return { herausgenommenAus };
|
|
1580
1746
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
1581
1747
|
if (!zeiger.skills.delete(args.skill))
|
|
1582
|
-
return {
|
|
1748
|
+
return { herausgenommenAus };
|
|
1583
1749
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
1584
|
-
return {
|
|
1750
|
+
return { herausgenommenAus };
|
|
1585
1751
|
}
|
|
1586
1752
|
/**
|
|
1587
1753
|
* Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
|
|
@@ -1996,9 +2162,15 @@ export class Anwendungsfaelle {
|
|
|
1996
2162
|
? { verwaiste: [], gefragt: false }
|
|
1997
2163
|
: await this.verwaisteAufraeumen(checkouts, rueckfragen);
|
|
1998
2164
|
const doctorErgebnis = nurSchluessel !== undefined
|
|
1999
|
-
? { fehlendeArbeitsverzeichnisse: [], vergessen: [] }
|
|
2000
|
-
: await this.
|
|
2001
|
-
|
|
2165
|
+
? { fehlendeArbeitsverzeichnisse: [], vergessen: [], sops: [] }
|
|
2166
|
+
: await this.doctorPruefen(rueckfragen);
|
|
2167
|
+
// Die SOPS-Befunde (E115) kommen in denselben Eintrag; ein eingeschränkter Lauf prüft sie nicht
|
|
2168
|
+
// und lässt die gemerkte Liste stehen.
|
|
2169
|
+
const sops = nurSchluessel !== undefined ? undefined : await this.sopsEreignisse(doctorErgebnis.sops, true);
|
|
2170
|
+
await this.verlaufVermerken(ausloeser, [
|
|
2171
|
+
...this.aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis),
|
|
2172
|
+
...(sops?.ereignisse ?? []),
|
|
2173
|
+
], sops?.gemerkt);
|
|
2002
2174
|
return { checkouts, pluginHarnesses, pluginFehler, bundles, aufraeumen, doctor: doctorErgebnis };
|
|
2003
2175
|
}
|
|
2004
2176
|
/**
|
|
@@ -2049,13 +2221,23 @@ export class Anwendungsfaelle {
|
|
|
2049
2221
|
ereignisse.push({ art: 'arbeitsverzeichnis-vergessen', pfad });
|
|
2050
2222
|
return ereignisse;
|
|
2051
2223
|
}
|
|
2052
|
-
/**
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2224
|
+
/**
|
|
2225
|
+
* Neuen Eintrag vorn anfügen, nur wenn Ereignisse da sind (E80). Kappen übernimmt der Adapter.
|
|
2226
|
+
* `sopsGemerkt` (E115) ersetzt die gemerkten SOPS-Meldungen; hat sich nur diese Liste geändert,
|
|
2227
|
+
* wird sie ohne neuen Eintrag geschrieben.
|
|
2228
|
+
*/
|
|
2229
|
+
async verlaufVermerken(ausloeser, ereignisse, sopsGemerkt) {
|
|
2056
2230
|
const daten = await this.p.verlauf.lesen();
|
|
2231
|
+
const sopsGeaendert = sopsGemerkt !== undefined && JSON.stringify(sopsGemerkt) !== JSON.stringify(daten.gemerkt.sops ?? []);
|
|
2232
|
+
if (ereignisse.length === 0 && !sopsGeaendert)
|
|
2233
|
+
return;
|
|
2234
|
+
const gemerkt = sopsGeaendert ? { ...daten.gemerkt, sops: sopsGemerkt } : daten.gemerkt;
|
|
2235
|
+
if (ereignisse.length === 0) {
|
|
2236
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: daten.eintraege });
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2057
2239
|
const eintrag = { zeit: new Date().toISOString(), ausloeser, ereignisse };
|
|
2058
|
-
await this.p.verlauf.schreiben({ gemerkt
|
|
2240
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: [eintrag, ...daten.eintraege] });
|
|
2059
2241
|
}
|
|
2060
2242
|
/** Der Verlauf, neueste zuerst (E80). */
|
|
2061
2243
|
async verlauf() {
|
|
@@ -2580,14 +2762,20 @@ export class Anwendungsfaelle {
|
|
|
2580
2762
|
for (const ei of eintraege)
|
|
2581
2763
|
if (ei.art === 'fremd')
|
|
2582
2764
|
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
|
|
2765
|
+
// Bundles dieses Ziels (E69), rein aus dem Zustand: verlinkt sind die Mitglieder, die dort
|
|
2766
|
+
// auch wirklich noch liegen; herausgenommene und von Hand gelöschte stehen als nicht
|
|
2767
|
+
// verlinkt daneben (E114 L3), die Gruppe zeigt alle.
|
|
2585
2768
|
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
|
-
|
|
2769
|
+
const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) => {
|
|
2770
|
+
const herausgenommen = i.herausgenommen ?? [];
|
|
2771
|
+
return {
|
|
2772
|
+
name: i.bundle,
|
|
2773
|
+
quelle: i.quelle,
|
|
2774
|
+
skills: i.skills.filter((name) => eigeneNamen.has(name)),
|
|
2775
|
+
nichtVerlinkt: [...i.skills.filter((name) => !eigeneNamen.has(name)), ...herausgenommen],
|
|
2776
|
+
herausgenommen,
|
|
2777
|
+
};
|
|
2778
|
+
});
|
|
2591
2779
|
ergebnis.push({
|
|
2592
2780
|
ziel,
|
|
2593
2781
|
verzeichnis,
|