hmgr 0.0.6 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/agents.js +14 -5
- package/dist/adapter/bundle-yaml.js +87 -0
- package/dist/adapter/claude-code.js +53 -12
- package/dist/adapter/dateisystem.js +85 -43
- package/dist/adapter/git.js +148 -0
- package/dist/adapter/heime.js +76 -0
- package/dist/adapter/hermes.js +16 -7
- package/dist/adapter/npm-registry.js +61 -0
- package/dist/adapter/paket-version.js +25 -0
- package/dist/adapter/umgebung.js +6 -1
- package/dist/adapter/verlauf-json.js +39 -0
- package/dist/adapter/wsl.js +212 -0
- package/dist/adapter/zustand-json.js +40 -12
- package/dist/cli/hilfe.js +197 -11
- package/dist/cli/main.js +376 -21
- package/dist/gui/hmgr-cli.js +44 -0
- package/dist/gui/main.js +445 -4
- package/dist/gui/preload.cjs +30 -2
- package/dist/gui/renderer/index.html +79 -16
- package/dist/gui/renderer/renderer.js +807 -286
- package/dist/gui/renderer/styles.css +89 -16
- package/dist/kern/anwendungsfaelle.js +687 -18
- package/dist/kern/modell.js +192 -1
- package/dist/kern/version.js +13 -0
- package/dist/ports/dateizugriff.js +1 -0
- package/dist/ports/harness.js +4 -0
- package/dist/ports/heime.js +1 -0
- package/dist/ports/verlauf.js +4 -0
- package/dist/ports/zustand.js +16 -1
- package/package.json +26 -5
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
3
|
+
import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
4
4
|
import { passenderRef } from "./version.js";
|
|
5
|
-
import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, } from "../ports/harness.js";
|
|
5
|
+
import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, kannZusaetzlicheHeime, } from "../ports/harness.js";
|
|
6
6
|
/**
|
|
7
7
|
* Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
|
|
8
8
|
* Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
|
|
@@ -83,6 +83,19 @@ export class Anwendungsfaelle {
|
|
|
83
83
|
throw new HarnessNichtVorhanden(name);
|
|
84
84
|
return h;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Wächter vor jedem neu angelegten Link (E76): in ein Ziel eines deaktivierten Harness legt hmgr
|
|
88
|
+
* keinen. Seit E74 zeigt keine Ansicht solche Ziele, der Link wäre unsichtbarer Zustand. Sitzt in
|
|
89
|
+
* `linkLegen` und `bundleInstallieren`, also vor jeder Wirkung und für CLI wie GUI gleich.
|
|
90
|
+
* Bewusst nicht in `linkAnlegen`: über den laufen auch Abgleich und Aktualisierung, und die
|
|
91
|
+
* pflegen bestehende Links weiter, statt neue anzulegen.
|
|
92
|
+
*/
|
|
93
|
+
async harnessAktivGeprueft(name) {
|
|
94
|
+
const harness = await this.harnessGeprueft(name);
|
|
95
|
+
if (!(await this.aktiveHarnesses()).includes(name))
|
|
96
|
+
throw new HarnessInaktiv(name);
|
|
97
|
+
return harness;
|
|
98
|
+
}
|
|
86
99
|
/** Harnesses, die auf dem Rechner liegen. Alle anderen überspringt hmgr. */
|
|
87
100
|
async vorhandeneHarnesses() {
|
|
88
101
|
const ergebnis = [];
|
|
@@ -103,6 +116,130 @@ export class Anwendungsfaelle {
|
|
|
103
116
|
harnessTypen() {
|
|
104
117
|
return this.p.harnesses.map((h) => h.name);
|
|
105
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Die Home-Ziele eines Harness (E78, F88): das primäre und jedes zusätzliche Homedir, jedes als
|
|
121
|
+
* eigenständiges Ziel. Eine Stelle für alle Aufzählungen — Status, Säubern, GUI —, damit ein
|
|
122
|
+
* zusätzliches Homedir nirgends versehentlich fehlt. Ein `wsl:`-Heim im Zustand einer anderen
|
|
123
|
+
* Plattform lässt der Port weg (Entscheidung 7), hier kommt es dann gar nicht erst an.
|
|
124
|
+
*/
|
|
125
|
+
homeZiele(harness) {
|
|
126
|
+
if (!harness.orte().includes('home'))
|
|
127
|
+
return [];
|
|
128
|
+
const ziele = [{ harness: harness.name, ort: { art: 'home' } }];
|
|
129
|
+
if (kannZusaetzlicheHeime(harness))
|
|
130
|
+
for (const heim of this.p.heime.zusaetzliche(harness.name))
|
|
131
|
+
ziele.push({ harness: harness.name, ort: { art: 'home', heim: heim.adresse } });
|
|
132
|
+
return ziele;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Die Homedirs aller Harness-Typen, für `hmgr harness home` und die GUI (E78). Unabhängig davon,
|
|
136
|
+
* ob der Harness aktiv oder installiert ist: die Einstellung gehört dem Nutzer, nicht dem Ist-Zustand.
|
|
137
|
+
*/
|
|
138
|
+
async harnessHeime() {
|
|
139
|
+
const zustand = await this.p.zustand.lesen();
|
|
140
|
+
return this.p.harnesses.map((harness) => {
|
|
141
|
+
const eintrag = zustand.harnessHeime.find((h) => h.harness === harness.name);
|
|
142
|
+
const primaer = {
|
|
143
|
+
pfad: this.p.heime.primaer(harness.name),
|
|
144
|
+
eingestellt: eintrag?.pfad !== undefined && eintrag.pfad.length > 0,
|
|
145
|
+
};
|
|
146
|
+
const zusaetzliche = this.p.heime
|
|
147
|
+
.zusaetzliche(harness.name)
|
|
148
|
+
.map((h) => ({ adresse: h.adresse, pfad: h.pfad, eingestellt: true }));
|
|
149
|
+
return {
|
|
150
|
+
harness: harness.name,
|
|
151
|
+
heime: [primaer, ...zusaetzliche],
|
|
152
|
+
kannZusaetzliche: kannZusaetzlicheHeime(harness),
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Der Eintrag dieses Harness im Zustand, notfalls neu angelegt (E78). */
|
|
157
|
+
static heimEintrag(zustand, harness) {
|
|
158
|
+
let eintrag = zustand.harnessHeime.find((h) => h.harness === harness);
|
|
159
|
+
if (eintrag === undefined) {
|
|
160
|
+
eintrag = { harness, zusaetzliche: [] };
|
|
161
|
+
zustand.harnessHeime.push(eintrag);
|
|
162
|
+
}
|
|
163
|
+
return eintrag;
|
|
164
|
+
}
|
|
165
|
+
/** Einträge ohne Inhalt fallen weg, damit die state.json nicht mit leeren Hüllen zuwächst. */
|
|
166
|
+
static heimeAufraeumen(zustand) {
|
|
167
|
+
zustand.harnessHeime = zustand.harnessHeime.filter((h) => (h.pfad !== undefined && h.pfad.length > 0) || h.zusaetzliche.length > 0);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Primäres Homedir umstellen (E78, F89): hmgr merkt sich den neuen Pfad und zeigt ab dann ihn.
|
|
171
|
+
* Was am alten Ort liegt, bleibt unberührt liegen — dasselbe Verhalten wie beim Deaktivieren
|
|
172
|
+
* (E74). Wer umziehen will, verlinkt am neuen Ort neu; einen Umzugsassistenten gibt es bewusst
|
|
173
|
+
* nicht. Der Pfad wird nicht geprüft: ein Homedir, das erst noch entsteht, ist ein gültiger Wunsch.
|
|
174
|
+
*/
|
|
175
|
+
async harnessHeimSetzen(harnessName, pfad) {
|
|
176
|
+
this.harness(harnessName); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
177
|
+
const aufgeloest = path.resolve(pfad);
|
|
178
|
+
const zustand = await this.p.zustand.lesen();
|
|
179
|
+
Anwendungsfaelle.heimEintrag(zustand, harnessName).pfad = aufgeloest;
|
|
180
|
+
await this.p.zustand.schreiben(zustand);
|
|
181
|
+
return aufgeloest;
|
|
182
|
+
}
|
|
183
|
+
/** Gegenstück zu `harnessHeimSetzen` (E78, F89): wieder das Home des Nutzers, nichts wird verschoben. */
|
|
184
|
+
async harnessHeimZuruecksetzen(harnessName) {
|
|
185
|
+
this.harness(harnessName);
|
|
186
|
+
const zustand = await this.p.zustand.lesen();
|
|
187
|
+
const eintrag = zustand.harnessHeime.find((h) => h.harness === harnessName);
|
|
188
|
+
if (eintrag === undefined || eintrag.pfad === undefined)
|
|
189
|
+
return;
|
|
190
|
+
delete eintrag.pfad;
|
|
191
|
+
Anwendungsfaelle.heimeAufraeumen(zustand);
|
|
192
|
+
await this.p.zustand.schreiben(zustand);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Zusätzliches Homedir hinzufügen (E78, F87): Adresse `wsl:<distro>`, ohne Benutzer-Option —
|
|
196
|
+
* wsl.exe startet als Standardbenutzer der Distro, derselbe, der Claude Code dort nutzt. Das Home
|
|
197
|
+
* wird einmal ermittelt und gespeichert, nicht bei jedem Lauf neu erfragt: sonst hinge jede
|
|
198
|
+
* Ansicht an einem startenden WSL. Der Aufrufer zeigt den ermittelten Pfad an.
|
|
199
|
+
*/
|
|
200
|
+
async harnessHeimHinzufuegen(harnessName, adresse) {
|
|
201
|
+
const harness = this.harness(harnessName);
|
|
202
|
+
if (!kannZusaetzlicheHeime(harness))
|
|
203
|
+
throw new HeimFaehigkeitFehlt(harnessName);
|
|
204
|
+
const zerlegt = heimAdresseZerlegen(adresse);
|
|
205
|
+
if (zerlegt === undefined)
|
|
206
|
+
throw new HeimAdresseUngueltig(adresse);
|
|
207
|
+
const normalisiert = heimAdresseAlsText(zerlegt);
|
|
208
|
+
// Plattformgrenze (Entscheidung 7): ein WSL-Heim lässt sich nur unter Windows anlegen und
|
|
209
|
+
// bedienen. Ohne die Prüfung stünde hier ein Eintrag, den derselbe Rechner nie wieder sieht.
|
|
210
|
+
if (this.p.umgebung.plattform() !== 'win32')
|
|
211
|
+
throw new HeimNurUnterWindows(normalisiert, this.p.umgebung.plattform());
|
|
212
|
+
const zustand = await this.p.zustand.lesen();
|
|
213
|
+
const eintrag = Anwendungsfaelle.heimEintrag(zustand, harnessName);
|
|
214
|
+
if (eintrag.zusaetzliche.some((h) => h.adresse === normalisiert))
|
|
215
|
+
throw new HeimBereitsVorhanden(harnessName, normalisiert);
|
|
216
|
+
const pfad = await this.p.wsl.home(zerlegt.distro);
|
|
217
|
+
eintrag.zusaetzliche.push({ adresse: normalisiert, pfad });
|
|
218
|
+
await this.p.zustand.schreiben(zustand);
|
|
219
|
+
return { adresse: normalisiert, pfad };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Zusätzliches Homedir entfernen (E78): hmgr vergisst es, die Links dort bleiben liegen — wie bei
|
|
223
|
+
* jedem anderen „nicht mehr gemanagt" (E74, F89). Wer sie los werden will, entfernt sie vorher.
|
|
224
|
+
*/
|
|
225
|
+
async harnessHeimEntfernen(harnessName, adresse) {
|
|
226
|
+
this.harness(harnessName);
|
|
227
|
+
const zerlegt = heimAdresseZerlegen(adresse);
|
|
228
|
+
const normalisiert = zerlegt === undefined ? adresse : heimAdresseAlsText(zerlegt);
|
|
229
|
+
const zustand = await this.p.zustand.lesen();
|
|
230
|
+
const eintrag = zustand.harnessHeime.find((h) => h.harness === harnessName);
|
|
231
|
+
if (eintrag === undefined || !eintrag.zusaetzliche.some((h) => h.adresse === normalisiert))
|
|
232
|
+
throw new HeimUnbekannt(harnessName, normalisiert);
|
|
233
|
+
eintrag.zusaetzliche = eintrag.zusaetzliche.filter((h) => h.adresse !== normalisiert);
|
|
234
|
+
Anwendungsfaelle.heimeAufraeumen(zustand);
|
|
235
|
+
await this.p.zustand.schreiben(zustand);
|
|
236
|
+
}
|
|
237
|
+
/** Die installierten WSL-Distros, für die Auswahl beim Hinzufügen (E78, F87). */
|
|
238
|
+
async wslDistros() {
|
|
239
|
+
if (this.p.umgebung.plattform() !== 'win32')
|
|
240
|
+
return [];
|
|
241
|
+
return await this.p.wsl.distros().catch(() => []);
|
|
242
|
+
}
|
|
106
243
|
/**
|
|
107
244
|
* Harness-Typen, die hmgr verwaltet (E38, GRILL.md Runde 2). Nur für sie richtet hmgr beim
|
|
108
245
|
* Arbeitsverzeichnis-Hinzufügen und beim Abgleich etwas ein. Standard: nur claude-code.
|
|
@@ -291,6 +428,8 @@ export class Anwendungsfaelle {
|
|
|
291
428
|
if (!kannMcp(harness))
|
|
292
429
|
continue;
|
|
293
430
|
const ebenen = harness.mcp.ebenen();
|
|
431
|
+
// Nur das primäre Homedir (E78): die claude-CLI dieses Rechners bedient die Konfiguration
|
|
432
|
+
// einer WSL-Distro nicht, dort gibt es keine MCP-Server zu verwalten.
|
|
294
433
|
if (ebenen.includes('home'))
|
|
295
434
|
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
296
435
|
if (ebenen.includes('profil') && kannProfile(harness))
|
|
@@ -354,9 +493,7 @@ export class Anwendungsfaelle {
|
|
|
354
493
|
async saeubern(harnessName) {
|
|
355
494
|
const harness = this.harness(harnessName); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
356
495
|
const zustand = await this.p.zustand.lesen();
|
|
357
|
-
const ziele = [];
|
|
358
|
-
if (harness.orte().includes('home'))
|
|
359
|
-
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
496
|
+
const ziele = [...this.homeZiele(harness)];
|
|
360
497
|
if (harness.orte().includes('profil') && kannProfile(harness))
|
|
361
498
|
for (const name of await harness.profile())
|
|
362
499
|
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
@@ -364,6 +501,10 @@ export class Anwendungsfaelle {
|
|
|
364
501
|
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
365
502
|
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
366
503
|
const zielErgebnisse = [];
|
|
504
|
+
// Mit den Links verschwinden auch die Bundle-Verweise dieser Ziele (E69): ein Verweis auf Links,
|
|
505
|
+
// die es nicht mehr gibt, wäre ein Rest, den niemand mehr sieht.
|
|
506
|
+
const nachZustand = await this.p.zustand.lesen();
|
|
507
|
+
let bundlesGeaendert = false;
|
|
367
508
|
for (const ziel of ziele) {
|
|
368
509
|
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
369
510
|
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
@@ -375,9 +516,13 @@ export class Anwendungsfaelle {
|
|
|
375
516
|
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
376
517
|
entfernteLinks.push(name);
|
|
377
518
|
}
|
|
519
|
+
if (Anwendungsfaelle.bundlesZerbrechen(nachZustand, ziel, entfernteLinks).length > 0)
|
|
520
|
+
bundlesGeaendert = true;
|
|
378
521
|
const konfigurationZurueckgebaut = kannZurueckbauen(harness) ? await harness.zurueckbauen(ziel.ort) : false;
|
|
379
522
|
zielErgebnisse.push({ ziel, entfernteLinks, konfigurationZurueckgebaut });
|
|
380
523
|
}
|
|
524
|
+
if (bundlesGeaendert)
|
|
525
|
+
await this.p.zustand.schreiben(nachZustand);
|
|
381
526
|
return { harness: harness.name, ziele: zielErgebnisse };
|
|
382
527
|
}
|
|
383
528
|
/**
|
|
@@ -513,6 +658,15 @@ export class Anwendungsfaelle {
|
|
|
513
658
|
throw new QuelleAufraeumenLokaleAenderungen(inventar.checkoutsMitLokalenAenderungen);
|
|
514
659
|
for (const link of inventar.links)
|
|
515
660
|
await this.p.ziele.linkEntfernen(link.ziel, link.verzeichnis, link.name);
|
|
661
|
+
// Wo dabei ein Bundle-Mitglied fiel, zerbricht das Bundle in diesem Ziel (E69), wie beim
|
|
662
|
+
// einzelnen `linkEntfernen`: die übrigen Links bleiben als Einzel-Installationen liegen.
|
|
663
|
+
const zustand = await this.p.zustand.lesen();
|
|
664
|
+
let bundlesGeaendert = false;
|
|
665
|
+
for (const link of inventar.links)
|
|
666
|
+
if (Anwendungsfaelle.bundlesZerbrechen(zustand, link.ziel, [link.name]).length > 0)
|
|
667
|
+
bundlesGeaendert = true;
|
|
668
|
+
if (bundlesGeaendert)
|
|
669
|
+
await this.p.zustand.schreiben(zustand);
|
|
516
670
|
for (const checkout of inventar.checkouts)
|
|
517
671
|
await this.p.quellen.entfernen(checkout);
|
|
518
672
|
return { entfernteLinks: inventar.links.length, entfernteCheckouts: inventar.checkouts.length };
|
|
@@ -551,7 +705,20 @@ export class Anwendungsfaelle {
|
|
|
551
705
|
try {
|
|
552
706
|
const ref = await this.p.quellen.standardRef(quelle);
|
|
553
707
|
const skills = await this.p.quellen.skills(quelle, ref);
|
|
554
|
-
|
|
708
|
+
// Änderungsdatum je Skill (E70, F70=O1), in einem Aufruf für die ganze Quelle. Ein
|
|
709
|
+
// Fehlschlag kostet nur die Daten, nicht den Katalog: der Reiter zeigt dann die Skills
|
|
710
|
+
// ohne Zusatzangabe.
|
|
711
|
+
const daten = await this.p.quellen
|
|
712
|
+
.letzteAenderungen(quelle, ref, skills.map((s) => s.pfad))
|
|
713
|
+
.catch(() => new Map());
|
|
714
|
+
ergebnis.push({
|
|
715
|
+
quelle,
|
|
716
|
+
ref,
|
|
717
|
+
skills: skills.map((skill) => {
|
|
718
|
+
const geaendert = daten.get(skill.pfad);
|
|
719
|
+
return geaendert === undefined ? skill : { ...skill, geaendert };
|
|
720
|
+
}),
|
|
721
|
+
});
|
|
555
722
|
}
|
|
556
723
|
catch (fehler) {
|
|
557
724
|
ergebnis.push({ quelle, skills: [], fehler: fehlertext(fehler) });
|
|
@@ -630,6 +797,359 @@ export class Anwendungsfaelle {
|
|
|
630
797
|
}
|
|
631
798
|
return skills;
|
|
632
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* Bundle-Katalog (E69): je bekannter Quelle ihre Bundle-Definitionen am Standard-Ref. Bundles
|
|
802
|
+
* sind Quell-Inhalt und werden beim Quellen-Scan entdeckt, es gibt keinen eigenen Katalog und
|
|
803
|
+
* keinen Pflegebefehl (F68). Eine nicht erreichbare Quelle bekommt einen Fehlervermerk, statt den
|
|
804
|
+
* Lauf abzubrechen, wie bei `katalog` und `marketplaces`.
|
|
805
|
+
*/
|
|
806
|
+
async bundleKatalog() {
|
|
807
|
+
const zustand = await this.p.zustand.lesen();
|
|
808
|
+
const ergebnis = [];
|
|
809
|
+
for (const quelle of zustand.quellen) {
|
|
810
|
+
try {
|
|
811
|
+
const ref = await this.p.quellen.standardRef(quelle);
|
|
812
|
+
const funde = await this.p.quellen.bundles(quelle, ref);
|
|
813
|
+
ergebnis.push({
|
|
814
|
+
quelle,
|
|
815
|
+
ref,
|
|
816
|
+
bundles: funde.flatMap((f) => (f.art === 'bundle' ? [f.bundle] : [])),
|
|
817
|
+
kaputte: funde.flatMap((f) => (f.art === 'fehler' ? [{ datei: f.datei, fehler: f.fehler }] : [])),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
catch (fehler) {
|
|
821
|
+
ergebnis.push({ quelle, bundles: [], kaputte: [], fehler: fehlertext(fehler) });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return ergebnis;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Bundle über alle bekannten Quellen suchen (E69). Mehrere Treffer sind nur mit Quellenhinweis
|
|
828
|
+
* erlaubt, dieselbe Regel wie bei Skills (E12). Eine nicht erreichbare Quelle fällt aus der Suche,
|
|
829
|
+
* statt sie abzubrechen — sonst fände kein einziges Bundle mehr statt, weil eine Quelle klemmt.
|
|
830
|
+
*/
|
|
831
|
+
async bundleFinden(name, quelleHinweis) {
|
|
832
|
+
const funde = [];
|
|
833
|
+
for (const eintrag of await this.bundleKatalog()) {
|
|
834
|
+
if (!quellePasst(eintrag.quelle, quelleHinweis))
|
|
835
|
+
continue;
|
|
836
|
+
for (const bundle of eintrag.bundles)
|
|
837
|
+
if (bundle.name === name)
|
|
838
|
+
funde.push(bundle);
|
|
839
|
+
}
|
|
840
|
+
if (funde.length === 0)
|
|
841
|
+
throw new BundleUnbekannt(name);
|
|
842
|
+
if (funde.length > 1)
|
|
843
|
+
throw new BundleMehrdeutig(name, funde.map((b) => b.quelle));
|
|
844
|
+
return funde[0];
|
|
845
|
+
}
|
|
846
|
+
/** Die Definition zu einer Installation, aus der Quelle, in der sie steht (E69, für den Abgleich). */
|
|
847
|
+
async bundleAusQuelle(name, quelle) {
|
|
848
|
+
const key = quelleSchluessel(quelle);
|
|
849
|
+
const gefuehrt = (await this.p.zustand.lesen()).quellen.find((q) => quelleSchluessel(q) === key);
|
|
850
|
+
if (gefuehrt === undefined)
|
|
851
|
+
throw new QuelleUnbekannt(quelle);
|
|
852
|
+
const ref = await this.p.quellen.standardRef(gefuehrt);
|
|
853
|
+
for (const fund of await this.p.quellen.bundles(gefuehrt, ref))
|
|
854
|
+
if (fund.art === 'bundle' && fund.bundle.name === name)
|
|
855
|
+
return fund.bundle;
|
|
856
|
+
throw new BundleUnbekannt(name);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Mitglieder einer Definition auflösen (E69, F66): je Mitglied die Quelle (die geführte, wenn hmgr
|
|
860
|
+
* sie schon kennt, sonst die Adresse aus der Definition), ihren Standard-Ref und den Skill am
|
|
861
|
+
* genannten Pfad. Schreibt nichts: eine noch unbekannte Quelle wird hier nur gelesen und kommt
|
|
862
|
+
* erst in den Zustand, wenn das ganze Bundle steht (F66b, alles oder nichts). Lässt sich ein
|
|
863
|
+
* Mitglied nicht auflösen, wirft der Aufruf mit allen Gründen auf einmal.
|
|
864
|
+
*/
|
|
865
|
+
async bundleAufloesen(bundle) {
|
|
866
|
+
const zustand = await this.p.zustand.lesen();
|
|
867
|
+
const gefuehrt = new Map(zustand.quellen.map((q) => [quelleSchluessel(q), q]));
|
|
868
|
+
const funde = [];
|
|
869
|
+
const neue = [];
|
|
870
|
+
const fehler = [];
|
|
871
|
+
for (const mitglied of bundle.mitglieder) {
|
|
872
|
+
const key = quelleSchluessel(mitglied.quelle);
|
|
873
|
+
const quelle = gefuehrt.get(key) ?? neue.find((q) => quelleSchluessel(q) === key) ?? mitglied.quelle;
|
|
874
|
+
const adresse = quelleAlsText(mitglied.quelle);
|
|
875
|
+
try {
|
|
876
|
+
const ref = await this.p.quellen.standardRef(quelle);
|
|
877
|
+
const skill = (await this.p.quellen.skills(quelle, ref)).find((s) => s.pfad === mitglied.pfad);
|
|
878
|
+
if (skill === undefined) {
|
|
879
|
+
fehler.push({ adresse, pfad: mitglied.pfad, grund: 'kein Skill an diesem Pfad' });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
funde.push({ quelle, ref, skill });
|
|
883
|
+
if (!gefuehrt.has(key) && !neue.some((q) => quelleSchluessel(q) === key))
|
|
884
|
+
neue.push(quelle);
|
|
885
|
+
}
|
|
886
|
+
catch (f) {
|
|
887
|
+
fehler.push({ adresse, pfad: mitglied.pfad, grund: fehlertext(f) });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (fehler.length > 0)
|
|
891
|
+
throw new BundleMitgliederFehlen(bundle.name, fehler);
|
|
892
|
+
return { funde, neue };
|
|
893
|
+
}
|
|
894
|
+
/** Ziel einer Bundle-Installation aus dem Zustand (E69). */
|
|
895
|
+
static installationZiel(installation) {
|
|
896
|
+
return { harness: installation.harness, ort: installation.ort };
|
|
897
|
+
}
|
|
898
|
+
/** Bundle-Installationen eines Ziels aus dem Zustand (E69). */
|
|
899
|
+
static installationenAn(zustand, ziel) {
|
|
900
|
+
const schluessel = zielSchluessel(ziel);
|
|
901
|
+
return zustand.bundleInstallationen.filter((i) => zielSchluessel(Anwendungsfaelle.installationZiel(i)) === schluessel);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Namenskonflikte am Ziel (E69, F67): Liegt dort schon derselbe Skill aus derselben Quelle als
|
|
905
|
+
* hmgr-Link, wird er als Mitglied übernommen — so wird aus Bestand ein Bundle, ohne erst Links von
|
|
906
|
+
* Hand zu entfernen. Alles andere ist ein Konflikt: ein anderer Skill gleichen Namens, ein fremder
|
|
907
|
+
* Eintrag, ein Link ohne bekannte Quelle, ein Link, der schon zu einem anderen Bundle gehört (den
|
|
908
|
+
* zu übernehmen zerbräche jenes, ohne dass es jemand sagt), und zwei Mitglieder desselben Bundles
|
|
909
|
+
* mit demselben Namen (am Ziel gibt es den Namen nur einmal, E12).
|
|
910
|
+
*/
|
|
911
|
+
async bundleKonflikte(bundle, ziel, verzeichnis, funde) {
|
|
912
|
+
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
913
|
+
const nachName = new Map();
|
|
914
|
+
for (const eintrag of eintraege)
|
|
915
|
+
nachName.set(eintrag.art === 'link' ? eintrag.link.name : eintrag.name, eintrag);
|
|
916
|
+
const zustand = await this.p.zustand.lesen();
|
|
917
|
+
const eigenerSchluessel = bundleSchluessel(bundle);
|
|
918
|
+
const fremdesBundle = new Map();
|
|
919
|
+
for (const installation of Anwendungsfaelle.installationenAn(zustand, ziel)) {
|
|
920
|
+
if (bundleSchluessel({ name: installation.bundle, quelle: installation.quelle }) === eigenerSchluessel)
|
|
921
|
+
continue;
|
|
922
|
+
for (const name of installation.skills)
|
|
923
|
+
fremdesBundle.set(name, installation.bundle);
|
|
924
|
+
}
|
|
925
|
+
const konflikte = [];
|
|
926
|
+
const uebernommen = [];
|
|
927
|
+
const gesehen = new Set();
|
|
928
|
+
for (const fund of funde) {
|
|
929
|
+
const name = fund.skill.name;
|
|
930
|
+
if (gesehen.has(name)) {
|
|
931
|
+
konflikte.push({ name, grund: 'das Bundle nennt zwei Skills dieses Namens, am Ziel gibt es ihn nur einmal' });
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
gesehen.add(name);
|
|
935
|
+
const eintrag = nachName.get(name);
|
|
936
|
+
if (eintrag === undefined)
|
|
937
|
+
continue;
|
|
938
|
+
if (eintrag.art === 'fremd') {
|
|
939
|
+
konflikte.push({ name, grund: 'dort liegt ein fremder Eintrag' });
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (eintrag.art === 'link-ohne-quelle') {
|
|
943
|
+
konflikte.push({ name, grund: 'dort liegt ein Link, dessen Quelle hmgr nicht kennt' });
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const gleicheQuelle = quelleSchluessel(eintrag.link.checkout.quelle) === quelleSchluessel(fund.quelle);
|
|
947
|
+
if (!gleicheQuelle || eintrag.link.skill.pfad !== fund.skill.pfad) {
|
|
948
|
+
konflikte.push({
|
|
949
|
+
name,
|
|
950
|
+
grund: `dort liegt schon ein Link aus ${quelleAlsText(eintrag.link.checkout.quelle)} (${eintrag.link.skill.pfad})`,
|
|
951
|
+
});
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const anderes = fremdesBundle.get(name);
|
|
955
|
+
if (anderes !== undefined) {
|
|
956
|
+
konflikte.push({ name, grund: `der Link gehört zum Bundle „${anderes}"` });
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
uebernommen.push(name);
|
|
960
|
+
}
|
|
961
|
+
return { konflikte, uebernommen };
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Quellen aus einer Bundle-Definition anlegen (E69, F66), nachdem alles geprüft ist. Der
|
|
965
|
+
* Anzeigename ist der abgeleitete, bei Kollision der nächste freie (dieselbe Regel wie bei der
|
|
966
|
+
* Übernahme, E31): hier nennt niemand einen Namen, und ein Abbruch am Anzeigenamen wäre die
|
|
967
|
+
* falsche Sorte Hürde.
|
|
968
|
+
*/
|
|
969
|
+
async bundleQuellenAnlegen(neue) {
|
|
970
|
+
const angelegt = [];
|
|
971
|
+
for (const quelle of neue) {
|
|
972
|
+
const zustand = await this.p.zustand.lesen();
|
|
973
|
+
const key = quelleSchluessel(quelle);
|
|
974
|
+
if (zustand.quellen.some((q) => quelleSchluessel(q) === key))
|
|
975
|
+
continue;
|
|
976
|
+
const name = quelleNameFrei(quelleNameAbleiten(quelle), zustand.quellen);
|
|
977
|
+
const { quelle: gefuehrt } = await this.quelleHinzufuegen(quelle, name);
|
|
978
|
+
angelegt.push(gefuehrt);
|
|
979
|
+
}
|
|
980
|
+
return angelegt;
|
|
981
|
+
}
|
|
982
|
+
/** Die Zugehörigkeit der Links eines Bundles im Ziel merken (E69), einen alten Eintrag ersetzend. */
|
|
983
|
+
async bundleInstallationMerken(ziel, bundle, skills) {
|
|
984
|
+
const zustand = await this.p.zustand.lesen();
|
|
985
|
+
const schluessel = zielSchluessel(ziel);
|
|
986
|
+
const eigener = bundleSchluessel(bundle);
|
|
987
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((i) => zielSchluessel(Anwendungsfaelle.installationZiel(i)) !== schluessel ||
|
|
988
|
+
bundleSchluessel({ name: i.bundle, quelle: i.quelle }) !== eigener);
|
|
989
|
+
zustand.bundleInstallationen.push({
|
|
990
|
+
harness: ziel.harness,
|
|
991
|
+
ort: ziel.ort,
|
|
992
|
+
bundle: bundle.name,
|
|
993
|
+
quelle: bundle.quelle,
|
|
994
|
+
skills,
|
|
995
|
+
});
|
|
996
|
+
await this.p.zustand.schreiben(zustand);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Zerbrechen (E69): Wird ein einzelner Link entfernt, der zu einem Bundle gehört, zerbricht das
|
|
1000
|
+
* Bundle in diesem Ziel — die übrigen Links werden Einzel-Installationen, der Bundle-Verweis
|
|
1001
|
+
* fällt weg. Ändert nur den übergebenen Zustand, geschrieben wird er vom Aufrufer, der ohnehin
|
|
1002
|
+
* schreibt. Gibt die Namen der zerbrochenen Bundles zurück, damit die Oberfläche es sagen kann.
|
|
1003
|
+
*/
|
|
1004
|
+
static bundlesZerbrechen(zustand, ziel, namen) {
|
|
1005
|
+
const schluessel = zielSchluessel(ziel);
|
|
1006
|
+
const zerbrochen = [];
|
|
1007
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((installation) => {
|
|
1008
|
+
if (zielSchluessel(Anwendungsfaelle.installationZiel(installation)) !== schluessel)
|
|
1009
|
+
return true;
|
|
1010
|
+
if (!installation.skills.some((name) => namen.includes(name)))
|
|
1011
|
+
return true;
|
|
1012
|
+
zerbrochen.push(installation.bundle);
|
|
1013
|
+
return false;
|
|
1014
|
+
});
|
|
1015
|
+
return zerbrochen;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Bundle installieren (E69): das Bundle als Ganzes in ein Ziel. Erst wird alles geprüft — jedes
|
|
1019
|
+
* Mitglied auflösbar (F66b), kein Namenskonflikt am Ziel (F67) —, dann werden fehlende Quellen
|
|
1020
|
+
* angelegt und die Links gelegt. Scheitert die Prüfung, ist nichts geschehen: weder ein Link noch
|
|
1021
|
+
* ein Eintrag im Zustand, denn ein halbes Bundle wäre stilles Fehlen von Skills (B13).
|
|
1022
|
+
*
|
|
1023
|
+
* Der Zeiger bleibt außen vor: Bundles stehen im Zustand, nicht in der composer.json (E69), also
|
|
1024
|
+
* legt der Aufruf seine Links wie mit `--local` und trifft die Sperre aus E57 nie.
|
|
1025
|
+
*/
|
|
1026
|
+
async bundleInstallieren(args) {
|
|
1027
|
+
const harness = await this.harnessAktivGeprueft(args.ziel.harness);
|
|
1028
|
+
if (!harness.orte().includes(args.ziel.ort.art))
|
|
1029
|
+
throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
1030
|
+
const bundle = await this.bundleFinden(args.bundle, args.quelle);
|
|
1031
|
+
const { funde, neue } = await this.bundleAufloesen(bundle);
|
|
1032
|
+
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
1033
|
+
const { konflikte, uebernommen } = await this.bundleKonflikte(bundle, args.ziel, verzeichnis, funde);
|
|
1034
|
+
if (konflikte.length > 0)
|
|
1035
|
+
throw new BundleNamenskonflikte(bundle.name, args.ziel, konflikte);
|
|
1036
|
+
const neueQuellen = await this.bundleQuellenAnlegen(neue);
|
|
1037
|
+
for (const fund of funde)
|
|
1038
|
+
await this.linkAnlegen(harness, args.ziel, fund);
|
|
1039
|
+
const namen = funde.map((f) => f.skill.name);
|
|
1040
|
+
await this.bundleInstallationMerken(args.ziel, bundle, namen);
|
|
1041
|
+
// Der Hinweis des Harness gilt dem Ort, also einmal für das ganze Bundle, nicht je Skill (E34).
|
|
1042
|
+
const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
|
|
1043
|
+
return {
|
|
1044
|
+
bundle,
|
|
1045
|
+
ziel: args.ziel,
|
|
1046
|
+
gelegt: namen.filter((n) => !uebernommen.includes(n)),
|
|
1047
|
+
uebernommen,
|
|
1048
|
+
neueQuellen,
|
|
1049
|
+
...(hinweis === undefined ? {} : { hinweis }),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/** Die Installation eines Bundles in einem Ziel, über seinen Namen (E69). */
|
|
1053
|
+
async installationFinden(bundleName, ziel) {
|
|
1054
|
+
const zustand = await this.p.zustand.lesen();
|
|
1055
|
+
const treffer = Anwendungsfaelle.installationenAn(zustand, ziel).filter((i) => i.bundle === bundleName);
|
|
1056
|
+
if (treffer.length === 0)
|
|
1057
|
+
throw new BundleNichtInstalliert(bundleName, ziel);
|
|
1058
|
+
// Zwei Quellen dürfen denselben Bundle-Namen führen (E69); sind beide hier installiert, muss
|
|
1059
|
+
// die Quelle genannt werden, wie bei einem mehrdeutigen Skill-Namen (E12).
|
|
1060
|
+
if (treffer.length > 1)
|
|
1061
|
+
throw new BundleMehrdeutig(bundleName, treffer.map((i) => i.quelle));
|
|
1062
|
+
return treffer[0];
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Bundle entfernen (E69): alle seine Links im Ziel und den Bundle-Verweis. Entfernt wird direkt
|
|
1066
|
+
* über den Ziele-Port, nicht über den Anwendungsfall `linkEntfernen` — der macht Zeiger-Arbeit
|
|
1067
|
+
* (E57) und ließe das Bundle bei jedem einzelnen Link zerbrechen, das es hier gerade verlässt.
|
|
1068
|
+
*/
|
|
1069
|
+
async bundleEntfernen(args) {
|
|
1070
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
1071
|
+
const installation = await this.installationFinden(args.bundle, args.ziel);
|
|
1072
|
+
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
1073
|
+
const eintraege = await this.p.ziele.eintraege(args.ziel, verzeichnis).catch(() => []);
|
|
1074
|
+
const eigene = new Set(eintraege.filter((e) => e.art !== 'fremd').map((e) => (e.art === 'link' ? e.link.name : e.name)));
|
|
1075
|
+
const entfernt = [];
|
|
1076
|
+
for (const name of installation.skills) {
|
|
1077
|
+
// Was nicht mehr liegt, wird nicht gemeldet: ein von Hand gelöschter Link ist kein Fehler.
|
|
1078
|
+
if (!eigene.has(name))
|
|
1079
|
+
continue;
|
|
1080
|
+
await this.p.ziele.linkEntfernen(args.ziel, verzeichnis, name);
|
|
1081
|
+
entfernt.push(name);
|
|
1082
|
+
}
|
|
1083
|
+
const zustand = await this.p.zustand.lesen();
|
|
1084
|
+
const schluessel = zielSchluessel(args.ziel);
|
|
1085
|
+
const eigenerSchluessel = bundleSchluessel({ name: installation.bundle, quelle: installation.quelle });
|
|
1086
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((i) => zielSchluessel(Anwendungsfaelle.installationZiel(i)) !== schluessel ||
|
|
1087
|
+
bundleSchluessel({ name: i.bundle, quelle: i.quelle }) !== eigenerSchluessel);
|
|
1088
|
+
await this.p.zustand.schreiben(zustand);
|
|
1089
|
+
return { bundle: installation.bundle, quelle: installation.quelle, ziel: args.ziel, entfernt };
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Bundle-Abgleich (E69): jedes installierte Bundle gegen seine Definition ziehen — neue Mitglieder
|
|
1093
|
+
* verlinken, entfallene Links wegnehmen, bestehende auf den aktuellen Stand ihrer Quelle legen.
|
|
1094
|
+
* Wirkt auf das Bundle als Ganzes: Lässt sich eine Definitionsänderung nicht vollständig nachziehen
|
|
1095
|
+
* (Quelle weg, Pfad kein Skill mehr, Namenskonflikt), bleibt das Bundle im betroffenen Ziel auf dem
|
|
1096
|
+
* alten Stand und der Lauf meldet es, statt es halb nachzuziehen (F66b).
|
|
1097
|
+
*/
|
|
1098
|
+
async bundleAbgleich(args) {
|
|
1099
|
+
const zustand = await this.p.zustand.lesen();
|
|
1100
|
+
const zielSchl = args?.ziel === undefined ? undefined : zielSchluessel(args.ziel);
|
|
1101
|
+
const installationen = zustand.bundleInstallationen.filter((i) => (args?.bundle === undefined || i.bundle === args.bundle) &&
|
|
1102
|
+
(zielSchl === undefined || zielSchluessel(Anwendungsfaelle.installationZiel(i)) === zielSchl));
|
|
1103
|
+
const ergebnis = [];
|
|
1104
|
+
for (const installation of installationen) {
|
|
1105
|
+
const ziel = Anwendungsfaelle.installationZiel(installation);
|
|
1106
|
+
const kopf = { ziel, bundle: installation.bundle, quelle: installation.quelle };
|
|
1107
|
+
try {
|
|
1108
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
1109
|
+
// Die Definition liegt in der Quelle: ohne Nachziehen bliebe eine Änderung unsichtbar,
|
|
1110
|
+
// dieselbe Vorsorge wie bei einer Versionseinschränkung (E33). Einmal je Lauf und Quelle,
|
|
1111
|
+
// ein Fehlschlag (kein Netz) arbeitet mit dem vorhandenen Stand weiter.
|
|
1112
|
+
await this.refsNachziehen(installation.quelle);
|
|
1113
|
+
const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
|
|
1114
|
+
const { funde, neue } = await this.bundleAufloesen(bundle);
|
|
1115
|
+
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
1116
|
+
const { konflikte } = await this.bundleKonflikte(bundle, ziel, verzeichnis, funde);
|
|
1117
|
+
if (konflikte.length > 0)
|
|
1118
|
+
throw new BundleNamenskonflikte(bundle.name, ziel, konflikte);
|
|
1119
|
+
const neueQuellen = await this.bundleQuellenAnlegen(neue);
|
|
1120
|
+
for (const fund of funde)
|
|
1121
|
+
await this.linkAnlegen(harness, ziel, fund);
|
|
1122
|
+
const namen = funde.map((f) => f.skill.name);
|
|
1123
|
+
const entfernt = [];
|
|
1124
|
+
for (const name of installation.skills) {
|
|
1125
|
+
if (namen.includes(name))
|
|
1126
|
+
continue;
|
|
1127
|
+
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
1128
|
+
entfernt.push(name);
|
|
1129
|
+
}
|
|
1130
|
+
await this.bundleInstallationMerken(ziel, bundle, namen);
|
|
1131
|
+
const gelegt = namen.filter((n) => !installation.skills.includes(n));
|
|
1132
|
+
ergebnis.push({
|
|
1133
|
+
...kopf,
|
|
1134
|
+
ergebnis: gelegt.length === 0 && entfernt.length === 0 ? 'unveraendert' : 'aktualisiert',
|
|
1135
|
+
gelegt,
|
|
1136
|
+
entfernt,
|
|
1137
|
+
neueQuellen,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
catch (fehler) {
|
|
1141
|
+
ergebnis.push({
|
|
1142
|
+
...kopf,
|
|
1143
|
+
ergebnis: 'fehlgeschlagen',
|
|
1144
|
+
gelegt: [],
|
|
1145
|
+
entfernt: [],
|
|
1146
|
+
neueQuellen: [],
|
|
1147
|
+
fehler: fehlertext(fehler),
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return ergebnis;
|
|
1152
|
+
}
|
|
633
1153
|
/**
|
|
634
1154
|
* Arbeitsverzeichnis hinzufügen (E21, E26): Zeiger suchen, Bindung je vorhandenem Harness merken.
|
|
635
1155
|
* Kein Zeiger heißt: Verzeichnis merken, keine Bindung, Hinweis. Mehrdeutig heißt: Fehler,
|
|
@@ -777,7 +1297,7 @@ export class Anwendungsfaelle {
|
|
|
777
1297
|
* hätte sie keinen Platz und der Aufruf schlägt fehl.
|
|
778
1298
|
*/
|
|
779
1299
|
async linkLegen(args) {
|
|
780
|
-
const harness = await this.
|
|
1300
|
+
const harness = await this.harnessAktivGeprueft(args.ziel.harness);
|
|
781
1301
|
if (!harness.orte().includes(args.ziel.ort.art))
|
|
782
1302
|
throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
783
1303
|
const version = args.version;
|
|
@@ -849,28 +1369,36 @@ export class Anwendungsfaelle {
|
|
|
849
1369
|
/**
|
|
850
1370
|
* Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
|
|
851
1371
|
* einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
|
|
1372
|
+
*
|
|
1373
|
+
* Gehörte der Link zu einem Bundle, zerbricht es in diesem Ziel (E69): die übrigen Links bleiben
|
|
1374
|
+
* als Einzel-Installationen liegen, der Bundle-Verweis fällt weg. `zerbrochen` nennt die Bundles,
|
|
1375
|
+
* damit die Oberfläche es sagen kann.
|
|
852
1376
|
*/
|
|
853
1377
|
async linkEntfernen(args) {
|
|
854
1378
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
855
1379
|
// Vor jeder Wirkung: kein Entfernen, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
856
1380
|
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
857
1381
|
await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
|
|
1382
|
+
const zustand = await this.p.zustand.lesen();
|
|
1383
|
+
const zerbrochen = Anwendungsfaelle.bundlesZerbrechen(zustand, args.ziel, [args.skill]);
|
|
1384
|
+
if (zerbrochen.length > 0)
|
|
1385
|
+
await this.p.zustand.schreiben(zustand);
|
|
858
1386
|
if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
|
|
859
1387
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
860
1388
|
if (zeigerPfad !== undefined)
|
|
861
|
-
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).' };
|
|
862
|
-
return {};
|
|
1389
|
+
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).', zerbrochen };
|
|
1390
|
+
return { zerbrochen };
|
|
863
1391
|
}
|
|
864
1392
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
865
|
-
return {};
|
|
1393
|
+
return { zerbrochen };
|
|
866
1394
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
867
1395
|
if (zeigerPfad === undefined)
|
|
868
|
-
return {};
|
|
1396
|
+
return { zerbrochen };
|
|
869
1397
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
870
1398
|
if (!zeiger.skills.delete(args.skill))
|
|
871
|
-
return {};
|
|
1399
|
+
return { zerbrochen };
|
|
872
1400
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
873
|
-
return {};
|
|
1401
|
+
return { zerbrochen };
|
|
874
1402
|
}
|
|
875
1403
|
/**
|
|
876
1404
|
* Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
|
|
@@ -1195,9 +1723,15 @@ export class Anwendungsfaelle {
|
|
|
1195
1723
|
* Checkouts, deren Quelle der Zustand nicht führt, bleiben ganz außen vor (E54): sie werden weder
|
|
1196
1724
|
* geholt noch als verwaist zum Löschen angeboten. Eine vergessene Quelle ist vergessen, ihre
|
|
1197
1725
|
* Checkouts und Links liegen unangetastet, bis die Quelle wieder hinzukommt.
|
|
1726
|
+
*
|
|
1727
|
+
* Schreibt am Ende einen Verlaufseintrag (E80), aber nur, wenn der Lauf mindestens ein Ereignis
|
|
1728
|
+
* ergeben hat: unveränderte oder übersprungene Checkouts und Bundles ohne Änderung erzeugen
|
|
1729
|
+
* nichts, ein leerer Lauf bleibt ganz aus dem Changelog. `ausloeser` steht mit im Eintrag, damit
|
|
1730
|
+
* der Verlauf zeigt, ob ein Fund vom automatischen Lauf, einem Klick oder der CLI kam.
|
|
1198
1731
|
*/
|
|
1199
1732
|
async aktualisieren(args) {
|
|
1200
1733
|
const rueckfragen = args?.rueckfragen ?? true;
|
|
1734
|
+
const ausloeser = args?.ausloeser ?? 'cli';
|
|
1201
1735
|
const bekannt = new Map((await this.p.zustand.lesen()).quellen.map((q) => [quelleSchluessel(q), q]));
|
|
1202
1736
|
const checkouts = [];
|
|
1203
1737
|
for (const checkout of await this.p.quellen.checkouts()) {
|
|
@@ -1211,7 +1745,14 @@ export class Anwendungsfaelle {
|
|
|
1211
1745
|
checkouts.push({
|
|
1212
1746
|
checkout: { ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) },
|
|
1213
1747
|
aktualisierung,
|
|
1748
|
+
geaenderteSkills: await this.geaenderteSkills(checkout, aktualisierung),
|
|
1214
1749
|
});
|
|
1750
|
+
// `aktualisieren` holt für einen Checkout dieselben Refs wie `refsNachziehen` (fetch auf dem
|
|
1751
|
+
// Basis-Clone). Ist das gelaufen, braucht der Bundle-Abgleich unten die Quelle nicht ein
|
|
1752
|
+
// zweites Mal zu holen. Ein übersprungener oder gescheiterter Checkout hat nichts geholt und
|
|
1753
|
+
// wird deshalb nicht vermerkt.
|
|
1754
|
+
if (aktualisierung.ergebnis === 'aktualisiert' || aktualisierung.ergebnis === 'unveraendert')
|
|
1755
|
+
this.nachgezogen.add(quelleSchluessel(checkout.quelle));
|
|
1215
1756
|
}
|
|
1216
1757
|
// Nach den Checkouts die Plugins nachziehen (E14).
|
|
1217
1758
|
const pluginHarnesses = [];
|
|
@@ -1221,7 +1762,126 @@ export class Anwendungsfaelle {
|
|
|
1221
1762
|
await harness.plugins.aktualisieren();
|
|
1222
1763
|
pluginHarnesses.push(harness.name);
|
|
1223
1764
|
}
|
|
1224
|
-
|
|
1765
|
+
// Bundles vor dem Aufräumen (E69, F64=O1): der Abgleich legt und entfernt Links, und erst
|
|
1766
|
+
// danach steht fest, in welchen Checkout noch ein Link zeigt. Ein Fehlschlag je Bundle steckt
|
|
1767
|
+
// in seinem Eintrag, er bricht den Lauf nicht ab — wie eine unerreichbare Quelle (E51).
|
|
1768
|
+
const bundles = await this.bundleAbgleich();
|
|
1769
|
+
const aufraeumen = await this.verwaisteAufraeumen(checkouts, rueckfragen);
|
|
1770
|
+
await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, bundles, aufraeumen));
|
|
1771
|
+
return { checkouts, pluginHarnesses, bundles, aufraeumen };
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Ereignisse eines Aktualisieren-Laufs für den Verlauf (E80): Plugins bleiben draußen, weil sich
|
|
1775
|
+
* eine Änderung dort nicht messen lässt (`claude plugin marketplace update` liefert keine
|
|
1776
|
+
* Auskunft) — die bisherige Zeile „Plugins aktualisiert" war deshalb geschönt.
|
|
1777
|
+
*/
|
|
1778
|
+
aktualisierungsEreignisse(checkouts, bundles, aufraeumen) {
|
|
1779
|
+
const ereignisse = [];
|
|
1780
|
+
for (const { checkout, aktualisierung, geaenderteSkills } of checkouts) {
|
|
1781
|
+
if (aktualisierung.ergebnis === 'aktualisiert')
|
|
1782
|
+
ereignisse.push({
|
|
1783
|
+
art: 'checkout',
|
|
1784
|
+
quelle: quelleAlsText(checkout.quelle),
|
|
1785
|
+
ref: checkout.ref,
|
|
1786
|
+
von: aktualisierung.von,
|
|
1787
|
+
nach: aktualisierung.nach,
|
|
1788
|
+
skills: geaenderteSkills,
|
|
1789
|
+
});
|
|
1790
|
+
else if (aktualisierung.ergebnis === 'fehlgeschlagen')
|
|
1791
|
+
ereignisse.push({
|
|
1792
|
+
art: 'fehler',
|
|
1793
|
+
betrifft: `${quelleAlsText(checkout.quelle)} @ ${checkout.ref}`,
|
|
1794
|
+
fehler: aktualisierung.fehler,
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
for (const eintrag of bundles) {
|
|
1798
|
+
if (eintrag.ergebnis === 'fehlgeschlagen')
|
|
1799
|
+
ereignisse.push({ art: 'fehler', betrifft: eintrag.bundle, fehler: eintrag.fehler ?? '' });
|
|
1800
|
+
else if (eintrag.gelegt.length > 0 || eintrag.entfernt.length > 0)
|
|
1801
|
+
ereignisse.push({
|
|
1802
|
+
art: 'bundle',
|
|
1803
|
+
bundle: eintrag.bundle,
|
|
1804
|
+
ziel: eintrag.ziel,
|
|
1805
|
+
gelegt: eintrag.gelegt,
|
|
1806
|
+
entfernt: eintrag.entfernt,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
for (const verwaist of aufraeumen.verwaiste)
|
|
1810
|
+
if (verwaist.geloescht)
|
|
1811
|
+
ereignisse.push({ art: 'aufgeraeumt', checkout: `${quelleAlsText(verwaist.checkout.quelle)} @ ${verwaist.checkout.ref}` });
|
|
1812
|
+
return ereignisse;
|
|
1813
|
+
}
|
|
1814
|
+
/** Neuen Eintrag vorn anfügen, nur wenn Ereignisse da sind (E80). Kappen übernimmt der Adapter. */
|
|
1815
|
+
async verlaufVermerken(ausloeser, ereignisse) {
|
|
1816
|
+
if (ereignisse.length === 0)
|
|
1817
|
+
return;
|
|
1818
|
+
const daten = await this.p.verlauf.lesen();
|
|
1819
|
+
const eintrag = { zeit: new Date().toISOString(), ausloeser, ereignisse };
|
|
1820
|
+
await this.p.verlauf.schreiben({ gemerkt: daten.gemerkt, eintraege: [eintrag, ...daten.eintraege] });
|
|
1821
|
+
}
|
|
1822
|
+
/** Der Verlauf, neueste zuerst (E80). */
|
|
1823
|
+
async verlauf() {
|
|
1824
|
+
return (await this.p.verlauf.lesen()).eintraege;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Merkt die installierte Version eines hmgr-Pakets (GUI oder CLI) und schreibt bei einem
|
|
1828
|
+
* Versionssprung einen Verlaufseintrag (E80). Der erste Aufruf ohne gemerkte Version merkt nur,
|
|
1829
|
+
* ohne Ereignis — sonst bekäme jede Erstinstallation ein irreführendes „installiert".
|
|
1830
|
+
*/
|
|
1831
|
+
async versionsSprungVermerken(paket, version, ausloeser) {
|
|
1832
|
+
const daten = await this.p.verlauf.lesen();
|
|
1833
|
+
const bisher = paket === 'gui' ? daten.gemerkt.gui : daten.gemerkt.cli;
|
|
1834
|
+
if (bisher === version)
|
|
1835
|
+
return;
|
|
1836
|
+
const gemerkt = { ...daten.gemerkt, [paket]: version };
|
|
1837
|
+
if (bisher === undefined) {
|
|
1838
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: daten.eintraege });
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const eintrag = {
|
|
1842
|
+
zeit: new Date().toISOString(),
|
|
1843
|
+
ausloeser,
|
|
1844
|
+
ereignisse: [{ art: 'hmgr-installiert', paket, version }],
|
|
1845
|
+
};
|
|
1846
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: [eintrag, ...daten.eintraege] });
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Merkt ein hmgr-eigenes Ereignis (GUI-Update geladen oder CLI-Update auf npm verfügbar) einmal
|
|
1850
|
+
* je neuer Version (E80): dieselbe Version taucht nicht in jedem Lauf erneut im Changelog auf.
|
|
1851
|
+
*/
|
|
1852
|
+
async hmgrEreignisVermerken(ereignis, ausloeser) {
|
|
1853
|
+
const daten = await this.p.verlauf.lesen();
|
|
1854
|
+
const feld = ereignis.art === 'gui-update-geladen' ? 'guiGeladen' : 'cliVerfuegbar';
|
|
1855
|
+
if (daten.gemerkt[feld] === ereignis.version)
|
|
1856
|
+
return;
|
|
1857
|
+
const gemerkt = { ...daten.gemerkt, [feld]: ereignis.version };
|
|
1858
|
+
const eintrag = { zeit: new Date().toISOString(), ausloeser, ereignisse: [ereignis] };
|
|
1859
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: [eintrag, ...daten.eintraege] });
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Welche Skills ein aktualisierter Checkout mitgebracht hat (E70). Die geänderten Pfade kommen
|
|
1863
|
+
* aus dem Vergleich von altem und neuem Commit, die Skills aus dem Checkout selbst: ein Skill,
|
|
1864
|
+
* der in dieser Quelle liegt, hier aber nicht ausgecheckt ist, geht den Nutzer nichts an. Ein
|
|
1865
|
+
* Skill zählt, wenn eine geänderte Datei in seinem Ordner liegt; ein Wurzel-Skill ist das ganze
|
|
1866
|
+
* Repo und zählt bei jeder Änderung.
|
|
1867
|
+
*
|
|
1868
|
+
* Nur bei `aktualisiert` gibt es zwei Commits zum Vergleichen. Scheitert die Ermittlung, bleibt
|
|
1869
|
+
* die Liste leer: der Bericht wird ungenauer, der Lauf nicht kaputt.
|
|
1870
|
+
*/
|
|
1871
|
+
async geaenderteSkills(checkout, aktualisierung) {
|
|
1872
|
+
if (aktualisierung.ergebnis !== 'aktualisiert')
|
|
1873
|
+
return [];
|
|
1874
|
+
const dateien = await this.p.quellen
|
|
1875
|
+
.geaenderteDateien(checkout.quelle, aktualisierung.von, aktualisierung.nach)
|
|
1876
|
+
.catch(() => []);
|
|
1877
|
+
if (dateien.length === 0)
|
|
1878
|
+
return [];
|
|
1879
|
+
const skills = await this.p.quellen.ausgecheckteSkills(checkout).catch(() => []);
|
|
1880
|
+
const namen = new Set();
|
|
1881
|
+
for (const skill of skills)
|
|
1882
|
+
if (dateien.some((datei) => skill.pfad === '' || datei === skill.pfad || datei.startsWith(`${skill.pfad}/`)))
|
|
1883
|
+
namen.add(skill.name);
|
|
1884
|
+
return [...namen].sort((a, b) => a.localeCompare(b, 'de'));
|
|
1225
1885
|
}
|
|
1226
1886
|
/**
|
|
1227
1887
|
* Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
|
|
@@ -1573,9 +2233,7 @@ export class Anwendungsfaelle {
|
|
|
1573
2233
|
};
|
|
1574
2234
|
const ergebnis = [];
|
|
1575
2235
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
1576
|
-
const ziele = [];
|
|
1577
|
-
if (harness.orte().includes('home'))
|
|
1578
|
-
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
2236
|
+
const ziele = [...this.homeZiele(harness)];
|
|
1579
2237
|
if (harness.orte().includes('profil') && kannProfile(harness))
|
|
1580
2238
|
for (const name of await harness.profile())
|
|
1581
2239
|
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
@@ -1585,7 +2243,9 @@ export class Anwendungsfaelle {
|
|
|
1585
2243
|
for (const ziel of ziele) {
|
|
1586
2244
|
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
1587
2245
|
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
1588
|
-
|
|
2246
|
+
// Plugins nur am primären Homedir (E78): die claude-CLI dieses Rechners kennt die
|
|
2247
|
+
// Plugin-Konfiguration einer WSL-Distro nicht, ihre user-Liste gehörte zum Windows-Home.
|
|
2248
|
+
const plugins = kannPlugins(harness) && !(ziel.ort.art === 'home' && ziel.ort.heim !== undefined)
|
|
1589
2249
|
? await harness.plugins.installierte(ziel).catch(() => [])
|
|
1590
2250
|
: undefined;
|
|
1591
2251
|
const zeiger = await this.zeigerFuerZiel(ziel);
|
|
@@ -1609,6 +2269,14 @@ export class Anwendungsfaelle {
|
|
|
1609
2269
|
for (const ei of eintraege)
|
|
1610
2270
|
if (ei.art === 'fremd')
|
|
1611
2271
|
uebernahme.set(ei.name, await this.erkennenFremd(harness, verzeichnis, ei, bekannt, true));
|
|
2272
|
+
// Bundles dieses Ziels (E69), rein aus dem Zustand: nur die Mitglieder, die dort auch
|
|
2273
|
+
// wirklich noch liegen — ein von Hand gelöschter Link soll die Gruppe nicht länger führen.
|
|
2274
|
+
const eigeneNamen = new Set(eintraege.filter((ei) => ei.art !== 'fremd').map((ei) => (ei.art === 'link' ? ei.link.name : ei.name)));
|
|
2275
|
+
const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) => ({
|
|
2276
|
+
name: i.bundle,
|
|
2277
|
+
quelle: i.quelle,
|
|
2278
|
+
skills: i.skills.filter((name) => eigeneNamen.has(name)),
|
|
2279
|
+
}));
|
|
1612
2280
|
ergebnis.push({
|
|
1613
2281
|
ziel,
|
|
1614
2282
|
verzeichnis,
|
|
@@ -1617,6 +2285,7 @@ export class Anwendungsfaelle {
|
|
|
1617
2285
|
gruende: await this.gruende(harness, ziel, eintraege, zeiger, marktplatzFunde),
|
|
1618
2286
|
ignoriert,
|
|
1619
2287
|
uebernahme,
|
|
2288
|
+
bundles,
|
|
1620
2289
|
istRegistriert,
|
|
1621
2290
|
kannSkillOrdner: skillOrdnerFaehig,
|
|
1622
2291
|
...(plugins === undefined ? {} : { plugins }),
|