hmgr 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/adapter/bundle-yaml.js +87 -0
- package/dist/adapter/checkout-pfad.js +10 -2
- package/dist/adapter/claude-code.js +231 -7
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +42 -4
- package/dist/adapter/git.js +182 -8
- package/dist/adapter/hermes.js +73 -1
- package/dist/adapter/marketplace-json.js +2 -1
- package/dist/adapter/mcp-katalog.js +124 -0
- package/dist/adapter/paket-version.js +25 -0
- package/dist/adapter/quelle-text.js +10 -1
- package/dist/adapter/skill-md.js +2 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +11 -3
- package/dist/cli/hilfe.js +197 -0
- package/dist/cli/main.js +255 -7
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +462 -19
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +1 -1
- package/dist/gui/preload.cjs +20 -1
- package/dist/gui/renderer/index.html +108 -7
- package/dist/gui/renderer/renderer.js +1066 -142
- package/dist/gui/renderer/styles.css +85 -2
- package/dist/kern/anwendungsfaelle.js +624 -12
- package/dist/kern/modell.js +221 -3
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +3 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/dist/ports/zustand.js +15 -1
- package/package.json +59 -2
|
@@ -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, 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, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
4
4
|
import { passenderRef } from "./version.js";
|
|
5
|
-
import { kannIstRegistriert, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, } from "../ports/harness.js";
|
|
5
|
+
import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, } 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.
|
|
@@ -24,6 +24,22 @@ function ignorierterEintragGleich(i, args) {
|
|
|
24
24
|
function fehlertext(fehler) {
|
|
25
25
|
return fehler instanceof Error ? fehler.message : String(fehler);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* `${VAR}`- und `${env:VAR}`-Verweise in einem MCP-Konfigblock, ohne Dubletten (E61, F39).
|
|
29
|
+
* Exportiert, damit die GUI dieselbe Prüfung für den Warn-Vermerk an der Katalog-Karte
|
|
30
|
+
* wiederverwenden kann (E61-Folgerunde), statt sie nachzubauen.
|
|
31
|
+
*/
|
|
32
|
+
export function variablenVerweise(konfig) {
|
|
33
|
+
const texte = 'command' in konfig
|
|
34
|
+
? [konfig.command, ...(konfig.args ?? []), ...Object.values(konfig.env ?? {})]
|
|
35
|
+
: [konfig.url, ...Object.values(konfig.headers ?? {})];
|
|
36
|
+
const namen = new Set();
|
|
37
|
+
const muster = /\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
38
|
+
for (const text of texte)
|
|
39
|
+
for (const treffer of text.matchAll(muster))
|
|
40
|
+
namen.add(treffer[1]);
|
|
41
|
+
return [...namen];
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* Quellenhinweis bei Mehrdeutigkeit (E12): Teilstring des Schlüssels oder der Schreibweise. Beides
|
|
29
45
|
* zählt, damit `host/besitzer/repo` genauso greift wie ein Stück der URL, unter der die Quelle
|
|
@@ -174,6 +190,160 @@ export class Anwendungsfaelle {
|
|
|
174
190
|
const harness = await this.harnessGeprueft(ziel.harness);
|
|
175
191
|
await this.harnessMitSkillOrdner(harness.name).skillOrdnerLoeschen(ziel.ort, pfad);
|
|
176
192
|
}
|
|
193
|
+
/** Ein Harness mit der Fähigkeit MCP-Server (E61), sonst `McpFaehigkeitFehlt`. */
|
|
194
|
+
harnessMitMcp(name) {
|
|
195
|
+
const harness = this.harness(name);
|
|
196
|
+
if (!kannMcp(harness))
|
|
197
|
+
throw new McpFaehigkeitFehlt(name);
|
|
198
|
+
return harness;
|
|
199
|
+
}
|
|
200
|
+
/** MCP-Katalog, unverändert wie in ~/.hmgr/mcp.yaml gepflegt (E61). hmgr schreibt ihn nie. */
|
|
201
|
+
async mcpKatalog() {
|
|
202
|
+
return await this.p.mcpKatalog.lesen();
|
|
203
|
+
}
|
|
204
|
+
async mcpEintragFinden(mcpName) {
|
|
205
|
+
const eintrag = (await this.p.mcpKatalog.lesen()).find((e) => e.name === mcpName);
|
|
206
|
+
if (eintrag === undefined)
|
|
207
|
+
throw new McpUnbekannt(mcpName);
|
|
208
|
+
return eintrag;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* F39: fehlende `${VAR}`-Verweise als Warnung, nie als Abbruch. Geprüft wird gegen die eigene
|
|
212
|
+
* Prozess-Umgebung und, wenn die Fähigkeit eine `.env`-Datei nennt (Hermes), zusätzlich gegen sie.
|
|
213
|
+
*/
|
|
214
|
+
async mcpVariablenWarnung(faehig, eintrag, ort) {
|
|
215
|
+
const verweise = variablenVerweise(eintrag.konfig);
|
|
216
|
+
if (verweise.length === 0)
|
|
217
|
+
return undefined;
|
|
218
|
+
const prozessEnv = this.p.umgebung.variablen();
|
|
219
|
+
const envPfad = faehig.mcp.envDatei?.(ort);
|
|
220
|
+
const zusatzEnv = envPfad === undefined ? {} : await this.p.umgebung.envDatei(envPfad);
|
|
221
|
+
const fehlend = verweise.filter((v) => !(v in prozessEnv) && !(v in zusatzEnv));
|
|
222
|
+
if (fehlend.length === 0)
|
|
223
|
+
return undefined;
|
|
224
|
+
const mehrzahl = fehlend.length > 1;
|
|
225
|
+
return (`hmgr kann nicht wissen, was die Shell beim Start des Harness setzt: ` +
|
|
226
|
+
`${mehrzahl ? 'Variablen' : 'Variable'} ${fehlend.join(', ')} ${mehrzahl ? 'sind' : 'ist'} weder in der ` +
|
|
227
|
+
'aktuellen Umgebung noch in einer bekannten .env-Datei zu finden.');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* MCP einhängen (E61): Katalog-Eintrag an ein Ziel legen, Projekt oder global. Die Fähigkeit hängt
|
|
231
|
+
* am Adapter (E16): ein Harness ohne MCP-Fähigkeit oder ohne diese Ebene ist ein Fehler. Ein
|
|
232
|
+
* fehlender `${VAR}`-Verweis bricht nicht ab, sondern kommt als Warnung zurück (F39).
|
|
233
|
+
*/
|
|
234
|
+
async mcpEinhaengen(args) {
|
|
235
|
+
const eintrag = await this.mcpEintragFinden(args.name);
|
|
236
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
237
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
238
|
+
if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
|
|
239
|
+
throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
240
|
+
await faehig.mcp.einhaengen(eintrag.name, eintrag.konfig, args.ziel.ort);
|
|
241
|
+
const warnung = await this.mcpVariablenWarnung(faehig, eintrag, args.ziel.ort);
|
|
242
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
243
|
+
return {
|
|
244
|
+
eintrag,
|
|
245
|
+
ziel: args.ziel,
|
|
246
|
+
...(hinweis === undefined ? {} : { hinweis }),
|
|
247
|
+
...(warnung === undefined ? {} : { warnung }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** MCP aushängen (E61): Gegenstück zu `mcpEinhaengen`, ohne F39-Prüfung. */
|
|
251
|
+
async mcpAushaengen(args) {
|
|
252
|
+
const eintrag = await this.mcpEintragFinden(args.name);
|
|
253
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
254
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
255
|
+
if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
|
|
256
|
+
throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
257
|
+
await faehig.mcp.aushaengen(eintrag.name, args.ziel.ort);
|
|
258
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
259
|
+
return { eintrag, ziel: args.ziel, ...(hinweis === undefined ? {} : { hinweis }) };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* MCP übernehmen (E64, F48=O2): einen Eintrag aus der geteilten Konfigurationsdatei eines Ortes
|
|
263
|
+
* (bei Claude Code die `.mcp.json` der Projektwurzel) in den privaten Bereich desselben Ortes
|
|
264
|
+
* holen — eintragen und aus der geteilten Datei entfernen. Der Konfigblock kommt 1:1 aus der
|
|
265
|
+
* Datei, nicht aus dem Katalog: es gibt Einträge, die der Katalog nicht kennt, und beide Sorten
|
|
266
|
+
* müssen gehen. Die Fähigkeit hängt am Adapter (E16), nicht am Namen: fehlt sie, kennt dieser
|
|
267
|
+
* Harness-Typ keine geteilte Datei.
|
|
268
|
+
*/
|
|
269
|
+
async mcpUebernehmen(args) {
|
|
270
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
271
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
272
|
+
const uebernehmen = faehig.mcp.geteiltenUebernehmen;
|
|
273
|
+
if (uebernehmen === undefined)
|
|
274
|
+
throw new McpUebernahmeNichtMoeglich(args.ziel);
|
|
275
|
+
const { datei } = await uebernehmen.call(faehig.mcp, args.name, args.ziel.ort);
|
|
276
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
277
|
+
return { name: args.name, ziel: args.ziel, datei, ...(hinweis === undefined ? {} : { hinweis }) };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Alle Orte, an denen MCP-Server grundsätzlich hängen können (E61): je Harness mit der Fähigkeit
|
|
281
|
+
* (E16, am Adapter abgefragt, nie am Namen) seine Ebenen aufgelöst zu tatsächlichen Zielen — Home,
|
|
282
|
+
* jedes bekannte Profil, jedes bekannte Arbeitsverzeichnis. Unabhängig davon, ob dort schon etwas
|
|
283
|
+
* hängt oder der Harness gerade aktiv ist (E38): das entscheidet der Aufrufer. Geteilt zwischen
|
|
284
|
+
* `mcpUebersicht` (dort mit `eingehaengte` abgeglichen) und `mcpPlaetze` (roh für den
|
|
285
|
+
* Einhängen-Dialog der GUI).
|
|
286
|
+
*/
|
|
287
|
+
async mcpZiele() {
|
|
288
|
+
const ziele = [];
|
|
289
|
+
const zustand = await this.p.zustand.lesen();
|
|
290
|
+
for (const harness of await this.vorhandeneHarnesses()) {
|
|
291
|
+
if (!kannMcp(harness))
|
|
292
|
+
continue;
|
|
293
|
+
const ebenen = harness.mcp.ebenen();
|
|
294
|
+
if (ebenen.includes('home'))
|
|
295
|
+
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
296
|
+
if (ebenen.includes('profil') && kannProfile(harness))
|
|
297
|
+
for (const name of await harness.profile())
|
|
298
|
+
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
299
|
+
if (ebenen.includes('arbeitsverzeichnis'))
|
|
300
|
+
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
301
|
+
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
302
|
+
}
|
|
303
|
+
return ziele;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Die Plätze für den Einhängen-Dialog der GUI (E61-Folgerunde, F42): dieselbe Liste wie in
|
|
307
|
+
* `mcpUebersicht`, aber roh, ohne den Abgleich mit Katalog-Einträgen. Die Fähigkeit hängt am
|
|
308
|
+
* Adapter, nicht am Harness-Namen (E16): agents kommt nie vor, weil es `mcp` nicht implementiert.
|
|
309
|
+
*/
|
|
310
|
+
async mcpPlaetze() {
|
|
311
|
+
return this.mcpZiele();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Übersicht für `hmgr mcp list` (E61): je Katalog-Eintrag, wo er eingehängt ist, dazu Einträge in
|
|
315
|
+
* Harness-Konfigurationen, die nicht im Katalog stehen (nur genannt, nicht angefasst).
|
|
316
|
+
*/
|
|
317
|
+
async mcpUebersicht() {
|
|
318
|
+
const katalog = (await this.p.mcpKatalog.lesen()).map((eintrag) => ({
|
|
319
|
+
eintrag,
|
|
320
|
+
eingehaengt: [],
|
|
321
|
+
geteilteFundorte: [],
|
|
322
|
+
}));
|
|
323
|
+
const nachName = new Map(katalog.map((k) => [k.eintrag.name, k]));
|
|
324
|
+
const unbekannt = [];
|
|
325
|
+
const unbekannteGeteilteFundorte = [];
|
|
326
|
+
for (const ziel of await this.mcpZiele()) {
|
|
327
|
+
const faehig = this.harnessMitMcp(ziel.harness);
|
|
328
|
+
const namen = await faehig.mcp.eingehaengte(ziel.ort).catch(() => []);
|
|
329
|
+
for (const mcpName of namen) {
|
|
330
|
+
const eintrag = nachName.get(mcpName);
|
|
331
|
+
if (eintrag !== undefined)
|
|
332
|
+
eintrag.eingehaengt.push(ziel);
|
|
333
|
+
else
|
|
334
|
+
unbekannt.push({ ziel, name: mcpName });
|
|
335
|
+
}
|
|
336
|
+
const geteilt = (await faehig.mcp.geteilteFundorte?.(ziel.ort).catch(() => [])) ?? [];
|
|
337
|
+
for (const fundort of geteilt) {
|
|
338
|
+
const eintrag = nachName.get(fundort.name);
|
|
339
|
+
if (eintrag !== undefined)
|
|
340
|
+
eintrag.geteilteFundorte.push({ ziel, datei: fundort.datei, scope: fundort.scope });
|
|
341
|
+
else
|
|
342
|
+
unbekannteGeteilteFundorte.push({ ziel, name: fundort.name, datei: fundort.datei, scope: fundort.scope });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { katalog, unbekannt, unbekannteGeteilteFundorte };
|
|
346
|
+
}
|
|
177
347
|
/**
|
|
178
348
|
* Säubern (E38): entfernt in allen Zielen dieses Harness-Typs (Home, Profile, bekannte
|
|
179
349
|
* Arbeitsverzeichnisse) die von hmgr angelegten Links und baut harness-eigene Einträge zurück, die
|
|
@@ -194,6 +364,10 @@ export class Anwendungsfaelle {
|
|
|
194
364
|
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
195
365
|
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
196
366
|
const zielErgebnisse = [];
|
|
367
|
+
// Mit den Links verschwinden auch die Bundle-Verweise dieser Ziele (E69): ein Verweis auf Links,
|
|
368
|
+
// die es nicht mehr gibt, wäre ein Rest, den niemand mehr sieht.
|
|
369
|
+
const nachZustand = await this.p.zustand.lesen();
|
|
370
|
+
let bundlesGeaendert = false;
|
|
197
371
|
for (const ziel of ziele) {
|
|
198
372
|
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
199
373
|
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
@@ -205,9 +379,13 @@ export class Anwendungsfaelle {
|
|
|
205
379
|
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
206
380
|
entfernteLinks.push(name);
|
|
207
381
|
}
|
|
382
|
+
if (Anwendungsfaelle.bundlesZerbrechen(nachZustand, ziel, entfernteLinks).length > 0)
|
|
383
|
+
bundlesGeaendert = true;
|
|
208
384
|
const konfigurationZurueckgebaut = kannZurueckbauen(harness) ? await harness.zurueckbauen(ziel.ort) : false;
|
|
209
385
|
zielErgebnisse.push({ ziel, entfernteLinks, konfigurationZurueckgebaut });
|
|
210
386
|
}
|
|
387
|
+
if (bundlesGeaendert)
|
|
388
|
+
await this.p.zustand.schreiben(nachZustand);
|
|
211
389
|
return { harness: harness.name, ziele: zielErgebnisse };
|
|
212
390
|
}
|
|
213
391
|
/**
|
|
@@ -343,6 +521,15 @@ export class Anwendungsfaelle {
|
|
|
343
521
|
throw new QuelleAufraeumenLokaleAenderungen(inventar.checkoutsMitLokalenAenderungen);
|
|
344
522
|
for (const link of inventar.links)
|
|
345
523
|
await this.p.ziele.linkEntfernen(link.ziel, link.verzeichnis, link.name);
|
|
524
|
+
// Wo dabei ein Bundle-Mitglied fiel, zerbricht das Bundle in diesem Ziel (E69), wie beim
|
|
525
|
+
// einzelnen `linkEntfernen`: die übrigen Links bleiben als Einzel-Installationen liegen.
|
|
526
|
+
const zustand = await this.p.zustand.lesen();
|
|
527
|
+
let bundlesGeaendert = false;
|
|
528
|
+
for (const link of inventar.links)
|
|
529
|
+
if (Anwendungsfaelle.bundlesZerbrechen(zustand, link.ziel, [link.name]).length > 0)
|
|
530
|
+
bundlesGeaendert = true;
|
|
531
|
+
if (bundlesGeaendert)
|
|
532
|
+
await this.p.zustand.schreiben(zustand);
|
|
346
533
|
for (const checkout of inventar.checkouts)
|
|
347
534
|
await this.p.quellen.entfernen(checkout);
|
|
348
535
|
return { entfernteLinks: inventar.links.length, entfernteCheckouts: inventar.checkouts.length };
|
|
@@ -381,7 +568,20 @@ export class Anwendungsfaelle {
|
|
|
381
568
|
try {
|
|
382
569
|
const ref = await this.p.quellen.standardRef(quelle);
|
|
383
570
|
const skills = await this.p.quellen.skills(quelle, ref);
|
|
384
|
-
|
|
571
|
+
// Änderungsdatum je Skill (E70, F70=O1), in einem Aufruf für die ganze Quelle. Ein
|
|
572
|
+
// Fehlschlag kostet nur die Daten, nicht den Katalog: der Reiter zeigt dann die Skills
|
|
573
|
+
// ohne Zusatzangabe.
|
|
574
|
+
const daten = await this.p.quellen
|
|
575
|
+
.letzteAenderungen(quelle, ref, skills.map((s) => s.pfad))
|
|
576
|
+
.catch(() => new Map());
|
|
577
|
+
ergebnis.push({
|
|
578
|
+
quelle,
|
|
579
|
+
ref,
|
|
580
|
+
skills: skills.map((skill) => {
|
|
581
|
+
const geaendert = daten.get(skill.pfad);
|
|
582
|
+
return geaendert === undefined ? skill : { ...skill, geaendert };
|
|
583
|
+
}),
|
|
584
|
+
});
|
|
385
585
|
}
|
|
386
586
|
catch (fehler) {
|
|
387
587
|
ergebnis.push({ quelle, skills: [], fehler: fehlertext(fehler) });
|
|
@@ -460,6 +660,359 @@ export class Anwendungsfaelle {
|
|
|
460
660
|
}
|
|
461
661
|
return skills;
|
|
462
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Bundle-Katalog (E69): je bekannter Quelle ihre Bundle-Definitionen am Standard-Ref. Bundles
|
|
665
|
+
* sind Quell-Inhalt und werden beim Quellen-Scan entdeckt, es gibt keinen eigenen Katalog und
|
|
666
|
+
* keinen Pflegebefehl (F68). Eine nicht erreichbare Quelle bekommt einen Fehlervermerk, statt den
|
|
667
|
+
* Lauf abzubrechen, wie bei `katalog` und `marketplaces`.
|
|
668
|
+
*/
|
|
669
|
+
async bundleKatalog() {
|
|
670
|
+
const zustand = await this.p.zustand.lesen();
|
|
671
|
+
const ergebnis = [];
|
|
672
|
+
for (const quelle of zustand.quellen) {
|
|
673
|
+
try {
|
|
674
|
+
const ref = await this.p.quellen.standardRef(quelle);
|
|
675
|
+
const funde = await this.p.quellen.bundles(quelle, ref);
|
|
676
|
+
ergebnis.push({
|
|
677
|
+
quelle,
|
|
678
|
+
ref,
|
|
679
|
+
bundles: funde.flatMap((f) => (f.art === 'bundle' ? [f.bundle] : [])),
|
|
680
|
+
kaputte: funde.flatMap((f) => (f.art === 'fehler' ? [{ datei: f.datei, fehler: f.fehler }] : [])),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
catch (fehler) {
|
|
684
|
+
ergebnis.push({ quelle, bundles: [], kaputte: [], fehler: fehlertext(fehler) });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return ergebnis;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Bundle über alle bekannten Quellen suchen (E69). Mehrere Treffer sind nur mit Quellenhinweis
|
|
691
|
+
* erlaubt, dieselbe Regel wie bei Skills (E12). Eine nicht erreichbare Quelle fällt aus der Suche,
|
|
692
|
+
* statt sie abzubrechen — sonst fände kein einziges Bundle mehr statt, weil eine Quelle klemmt.
|
|
693
|
+
*/
|
|
694
|
+
async bundleFinden(name, quelleHinweis) {
|
|
695
|
+
const funde = [];
|
|
696
|
+
for (const eintrag of await this.bundleKatalog()) {
|
|
697
|
+
if (!quellePasst(eintrag.quelle, quelleHinweis))
|
|
698
|
+
continue;
|
|
699
|
+
for (const bundle of eintrag.bundles)
|
|
700
|
+
if (bundle.name === name)
|
|
701
|
+
funde.push(bundle);
|
|
702
|
+
}
|
|
703
|
+
if (funde.length === 0)
|
|
704
|
+
throw new BundleUnbekannt(name);
|
|
705
|
+
if (funde.length > 1)
|
|
706
|
+
throw new BundleMehrdeutig(name, funde.map((b) => b.quelle));
|
|
707
|
+
return funde[0];
|
|
708
|
+
}
|
|
709
|
+
/** Die Definition zu einer Installation, aus der Quelle, in der sie steht (E69, für den Abgleich). */
|
|
710
|
+
async bundleAusQuelle(name, quelle) {
|
|
711
|
+
const key = quelleSchluessel(quelle);
|
|
712
|
+
const gefuehrt = (await this.p.zustand.lesen()).quellen.find((q) => quelleSchluessel(q) === key);
|
|
713
|
+
if (gefuehrt === undefined)
|
|
714
|
+
throw new QuelleUnbekannt(quelle);
|
|
715
|
+
const ref = await this.p.quellen.standardRef(gefuehrt);
|
|
716
|
+
for (const fund of await this.p.quellen.bundles(gefuehrt, ref))
|
|
717
|
+
if (fund.art === 'bundle' && fund.bundle.name === name)
|
|
718
|
+
return fund.bundle;
|
|
719
|
+
throw new BundleUnbekannt(name);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Mitglieder einer Definition auflösen (E69, F66): je Mitglied die Quelle (die geführte, wenn hmgr
|
|
723
|
+
* sie schon kennt, sonst die Adresse aus der Definition), ihren Standard-Ref und den Skill am
|
|
724
|
+
* genannten Pfad. Schreibt nichts: eine noch unbekannte Quelle wird hier nur gelesen und kommt
|
|
725
|
+
* erst in den Zustand, wenn das ganze Bundle steht (F66b, alles oder nichts). Lässt sich ein
|
|
726
|
+
* Mitglied nicht auflösen, wirft der Aufruf mit allen Gründen auf einmal.
|
|
727
|
+
*/
|
|
728
|
+
async bundleAufloesen(bundle) {
|
|
729
|
+
const zustand = await this.p.zustand.lesen();
|
|
730
|
+
const gefuehrt = new Map(zustand.quellen.map((q) => [quelleSchluessel(q), q]));
|
|
731
|
+
const funde = [];
|
|
732
|
+
const neue = [];
|
|
733
|
+
const fehler = [];
|
|
734
|
+
for (const mitglied of bundle.mitglieder) {
|
|
735
|
+
const key = quelleSchluessel(mitglied.quelle);
|
|
736
|
+
const quelle = gefuehrt.get(key) ?? neue.find((q) => quelleSchluessel(q) === key) ?? mitglied.quelle;
|
|
737
|
+
const adresse = quelleAlsText(mitglied.quelle);
|
|
738
|
+
try {
|
|
739
|
+
const ref = await this.p.quellen.standardRef(quelle);
|
|
740
|
+
const skill = (await this.p.quellen.skills(quelle, ref)).find((s) => s.pfad === mitglied.pfad);
|
|
741
|
+
if (skill === undefined) {
|
|
742
|
+
fehler.push({ adresse, pfad: mitglied.pfad, grund: 'kein Skill an diesem Pfad' });
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
funde.push({ quelle, ref, skill });
|
|
746
|
+
if (!gefuehrt.has(key) && !neue.some((q) => quelleSchluessel(q) === key))
|
|
747
|
+
neue.push(quelle);
|
|
748
|
+
}
|
|
749
|
+
catch (f) {
|
|
750
|
+
fehler.push({ adresse, pfad: mitglied.pfad, grund: fehlertext(f) });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (fehler.length > 0)
|
|
754
|
+
throw new BundleMitgliederFehlen(bundle.name, fehler);
|
|
755
|
+
return { funde, neue };
|
|
756
|
+
}
|
|
757
|
+
/** Ziel einer Bundle-Installation aus dem Zustand (E69). */
|
|
758
|
+
static installationZiel(installation) {
|
|
759
|
+
return { harness: installation.harness, ort: installation.ort };
|
|
760
|
+
}
|
|
761
|
+
/** Bundle-Installationen eines Ziels aus dem Zustand (E69). */
|
|
762
|
+
static installationenAn(zustand, ziel) {
|
|
763
|
+
const schluessel = zielSchluessel(ziel);
|
|
764
|
+
return zustand.bundleInstallationen.filter((i) => zielSchluessel(Anwendungsfaelle.installationZiel(i)) === schluessel);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Namenskonflikte am Ziel (E69, F67): Liegt dort schon derselbe Skill aus derselben Quelle als
|
|
768
|
+
* hmgr-Link, wird er als Mitglied übernommen — so wird aus Bestand ein Bundle, ohne erst Links von
|
|
769
|
+
* Hand zu entfernen. Alles andere ist ein Konflikt: ein anderer Skill gleichen Namens, ein fremder
|
|
770
|
+
* Eintrag, ein Link ohne bekannte Quelle, ein Link, der schon zu einem anderen Bundle gehört (den
|
|
771
|
+
* zu übernehmen zerbräche jenes, ohne dass es jemand sagt), und zwei Mitglieder desselben Bundles
|
|
772
|
+
* mit demselben Namen (am Ziel gibt es den Namen nur einmal, E12).
|
|
773
|
+
*/
|
|
774
|
+
async bundleKonflikte(bundle, ziel, verzeichnis, funde) {
|
|
775
|
+
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
776
|
+
const nachName = new Map();
|
|
777
|
+
for (const eintrag of eintraege)
|
|
778
|
+
nachName.set(eintrag.art === 'link' ? eintrag.link.name : eintrag.name, eintrag);
|
|
779
|
+
const zustand = await this.p.zustand.lesen();
|
|
780
|
+
const eigenerSchluessel = bundleSchluessel(bundle);
|
|
781
|
+
const fremdesBundle = new Map();
|
|
782
|
+
for (const installation of Anwendungsfaelle.installationenAn(zustand, ziel)) {
|
|
783
|
+
if (bundleSchluessel({ name: installation.bundle, quelle: installation.quelle }) === eigenerSchluessel)
|
|
784
|
+
continue;
|
|
785
|
+
for (const name of installation.skills)
|
|
786
|
+
fremdesBundle.set(name, installation.bundle);
|
|
787
|
+
}
|
|
788
|
+
const konflikte = [];
|
|
789
|
+
const uebernommen = [];
|
|
790
|
+
const gesehen = new Set();
|
|
791
|
+
for (const fund of funde) {
|
|
792
|
+
const name = fund.skill.name;
|
|
793
|
+
if (gesehen.has(name)) {
|
|
794
|
+
konflikte.push({ name, grund: 'das Bundle nennt zwei Skills dieses Namens, am Ziel gibt es ihn nur einmal' });
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
gesehen.add(name);
|
|
798
|
+
const eintrag = nachName.get(name);
|
|
799
|
+
if (eintrag === undefined)
|
|
800
|
+
continue;
|
|
801
|
+
if (eintrag.art === 'fremd') {
|
|
802
|
+
konflikte.push({ name, grund: 'dort liegt ein fremder Eintrag' });
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (eintrag.art === 'link-ohne-quelle') {
|
|
806
|
+
konflikte.push({ name, grund: 'dort liegt ein Link, dessen Quelle hmgr nicht kennt' });
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const gleicheQuelle = quelleSchluessel(eintrag.link.checkout.quelle) === quelleSchluessel(fund.quelle);
|
|
810
|
+
if (!gleicheQuelle || eintrag.link.skill.pfad !== fund.skill.pfad) {
|
|
811
|
+
konflikte.push({
|
|
812
|
+
name,
|
|
813
|
+
grund: `dort liegt schon ein Link aus ${quelleAlsText(eintrag.link.checkout.quelle)} (${eintrag.link.skill.pfad})`,
|
|
814
|
+
});
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const anderes = fremdesBundle.get(name);
|
|
818
|
+
if (anderes !== undefined) {
|
|
819
|
+
konflikte.push({ name, grund: `der Link gehört zum Bundle „${anderes}"` });
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
uebernommen.push(name);
|
|
823
|
+
}
|
|
824
|
+
return { konflikte, uebernommen };
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Quellen aus einer Bundle-Definition anlegen (E69, F66), nachdem alles geprüft ist. Der
|
|
828
|
+
* Anzeigename ist der abgeleitete, bei Kollision der nächste freie (dieselbe Regel wie bei der
|
|
829
|
+
* Übernahme, E31): hier nennt niemand einen Namen, und ein Abbruch am Anzeigenamen wäre die
|
|
830
|
+
* falsche Sorte Hürde.
|
|
831
|
+
*/
|
|
832
|
+
async bundleQuellenAnlegen(neue) {
|
|
833
|
+
const angelegt = [];
|
|
834
|
+
for (const quelle of neue) {
|
|
835
|
+
const zustand = await this.p.zustand.lesen();
|
|
836
|
+
const key = quelleSchluessel(quelle);
|
|
837
|
+
if (zustand.quellen.some((q) => quelleSchluessel(q) === key))
|
|
838
|
+
continue;
|
|
839
|
+
const name = quelleNameFrei(quelleNameAbleiten(quelle), zustand.quellen);
|
|
840
|
+
const { quelle: gefuehrt } = await this.quelleHinzufuegen(quelle, name);
|
|
841
|
+
angelegt.push(gefuehrt);
|
|
842
|
+
}
|
|
843
|
+
return angelegt;
|
|
844
|
+
}
|
|
845
|
+
/** Die Zugehörigkeit der Links eines Bundles im Ziel merken (E69), einen alten Eintrag ersetzend. */
|
|
846
|
+
async bundleInstallationMerken(ziel, bundle, skills) {
|
|
847
|
+
const zustand = await this.p.zustand.lesen();
|
|
848
|
+
const schluessel = zielSchluessel(ziel);
|
|
849
|
+
const eigener = bundleSchluessel(bundle);
|
|
850
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((i) => zielSchluessel(Anwendungsfaelle.installationZiel(i)) !== schluessel ||
|
|
851
|
+
bundleSchluessel({ name: i.bundle, quelle: i.quelle }) !== eigener);
|
|
852
|
+
zustand.bundleInstallationen.push({
|
|
853
|
+
harness: ziel.harness,
|
|
854
|
+
ort: ziel.ort,
|
|
855
|
+
bundle: bundle.name,
|
|
856
|
+
quelle: bundle.quelle,
|
|
857
|
+
skills,
|
|
858
|
+
});
|
|
859
|
+
await this.p.zustand.schreiben(zustand);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Zerbrechen (E69): Wird ein einzelner Link entfernt, der zu einem Bundle gehört, zerbricht das
|
|
863
|
+
* Bundle in diesem Ziel — die übrigen Links werden Einzel-Installationen, der Bundle-Verweis
|
|
864
|
+
* fällt weg. Ändert nur den übergebenen Zustand, geschrieben wird er vom Aufrufer, der ohnehin
|
|
865
|
+
* schreibt. Gibt die Namen der zerbrochenen Bundles zurück, damit die Oberfläche es sagen kann.
|
|
866
|
+
*/
|
|
867
|
+
static bundlesZerbrechen(zustand, ziel, namen) {
|
|
868
|
+
const schluessel = zielSchluessel(ziel);
|
|
869
|
+
const zerbrochen = [];
|
|
870
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((installation) => {
|
|
871
|
+
if (zielSchluessel(Anwendungsfaelle.installationZiel(installation)) !== schluessel)
|
|
872
|
+
return true;
|
|
873
|
+
if (!installation.skills.some((name) => namen.includes(name)))
|
|
874
|
+
return true;
|
|
875
|
+
zerbrochen.push(installation.bundle);
|
|
876
|
+
return false;
|
|
877
|
+
});
|
|
878
|
+
return zerbrochen;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Bundle installieren (E69): das Bundle als Ganzes in ein Ziel. Erst wird alles geprüft — jedes
|
|
882
|
+
* Mitglied auflösbar (F66b), kein Namenskonflikt am Ziel (F67) —, dann werden fehlende Quellen
|
|
883
|
+
* angelegt und die Links gelegt. Scheitert die Prüfung, ist nichts geschehen: weder ein Link noch
|
|
884
|
+
* ein Eintrag im Zustand, denn ein halbes Bundle wäre stilles Fehlen von Skills (B13).
|
|
885
|
+
*
|
|
886
|
+
* Der Zeiger bleibt außen vor: Bundles stehen im Zustand, nicht in der composer.json (E69), also
|
|
887
|
+
* legt der Aufruf seine Links wie mit `--local` und trifft die Sperre aus E57 nie.
|
|
888
|
+
*/
|
|
889
|
+
async bundleInstallieren(args) {
|
|
890
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
891
|
+
if (!harness.orte().includes(args.ziel.ort.art))
|
|
892
|
+
throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
893
|
+
const bundle = await this.bundleFinden(args.bundle, args.quelle);
|
|
894
|
+
const { funde, neue } = await this.bundleAufloesen(bundle);
|
|
895
|
+
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
896
|
+
const { konflikte, uebernommen } = await this.bundleKonflikte(bundle, args.ziel, verzeichnis, funde);
|
|
897
|
+
if (konflikte.length > 0)
|
|
898
|
+
throw new BundleNamenskonflikte(bundle.name, args.ziel, konflikte);
|
|
899
|
+
const neueQuellen = await this.bundleQuellenAnlegen(neue);
|
|
900
|
+
for (const fund of funde)
|
|
901
|
+
await this.linkAnlegen(harness, args.ziel, fund);
|
|
902
|
+
const namen = funde.map((f) => f.skill.name);
|
|
903
|
+
await this.bundleInstallationMerken(args.ziel, bundle, namen);
|
|
904
|
+
// Der Hinweis des Harness gilt dem Ort, also einmal für das ganze Bundle, nicht je Skill (E34).
|
|
905
|
+
const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
|
|
906
|
+
return {
|
|
907
|
+
bundle,
|
|
908
|
+
ziel: args.ziel,
|
|
909
|
+
gelegt: namen.filter((n) => !uebernommen.includes(n)),
|
|
910
|
+
uebernommen,
|
|
911
|
+
neueQuellen,
|
|
912
|
+
...(hinweis === undefined ? {} : { hinweis }),
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/** Die Installation eines Bundles in einem Ziel, über seinen Namen (E69). */
|
|
916
|
+
async installationFinden(bundleName, ziel) {
|
|
917
|
+
const zustand = await this.p.zustand.lesen();
|
|
918
|
+
const treffer = Anwendungsfaelle.installationenAn(zustand, ziel).filter((i) => i.bundle === bundleName);
|
|
919
|
+
if (treffer.length === 0)
|
|
920
|
+
throw new BundleNichtInstalliert(bundleName, ziel);
|
|
921
|
+
// Zwei Quellen dürfen denselben Bundle-Namen führen (E69); sind beide hier installiert, muss
|
|
922
|
+
// die Quelle genannt werden, wie bei einem mehrdeutigen Skill-Namen (E12).
|
|
923
|
+
if (treffer.length > 1)
|
|
924
|
+
throw new BundleMehrdeutig(bundleName, treffer.map((i) => i.quelle));
|
|
925
|
+
return treffer[0];
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Bundle entfernen (E69): alle seine Links im Ziel und den Bundle-Verweis. Entfernt wird direkt
|
|
929
|
+
* über den Ziele-Port, nicht über den Anwendungsfall `linkEntfernen` — der macht Zeiger-Arbeit
|
|
930
|
+
* (E57) und ließe das Bundle bei jedem einzelnen Link zerbrechen, das es hier gerade verlässt.
|
|
931
|
+
*/
|
|
932
|
+
async bundleEntfernen(args) {
|
|
933
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
934
|
+
const installation = await this.installationFinden(args.bundle, args.ziel);
|
|
935
|
+
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
936
|
+
const eintraege = await this.p.ziele.eintraege(args.ziel, verzeichnis).catch(() => []);
|
|
937
|
+
const eigene = new Set(eintraege.filter((e) => e.art !== 'fremd').map((e) => (e.art === 'link' ? e.link.name : e.name)));
|
|
938
|
+
const entfernt = [];
|
|
939
|
+
for (const name of installation.skills) {
|
|
940
|
+
// Was nicht mehr liegt, wird nicht gemeldet: ein von Hand gelöschter Link ist kein Fehler.
|
|
941
|
+
if (!eigene.has(name))
|
|
942
|
+
continue;
|
|
943
|
+
await this.p.ziele.linkEntfernen(args.ziel, verzeichnis, name);
|
|
944
|
+
entfernt.push(name);
|
|
945
|
+
}
|
|
946
|
+
const zustand = await this.p.zustand.lesen();
|
|
947
|
+
const schluessel = zielSchluessel(args.ziel);
|
|
948
|
+
const eigenerSchluessel = bundleSchluessel({ name: installation.bundle, quelle: installation.quelle });
|
|
949
|
+
zustand.bundleInstallationen = zustand.bundleInstallationen.filter((i) => zielSchluessel(Anwendungsfaelle.installationZiel(i)) !== schluessel ||
|
|
950
|
+
bundleSchluessel({ name: i.bundle, quelle: i.quelle }) !== eigenerSchluessel);
|
|
951
|
+
await this.p.zustand.schreiben(zustand);
|
|
952
|
+
return { bundle: installation.bundle, quelle: installation.quelle, ziel: args.ziel, entfernt };
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Bundle-Abgleich (E69): jedes installierte Bundle gegen seine Definition ziehen — neue Mitglieder
|
|
956
|
+
* verlinken, entfallene Links wegnehmen, bestehende auf den aktuellen Stand ihrer Quelle legen.
|
|
957
|
+
* Wirkt auf das Bundle als Ganzes: Lässt sich eine Definitionsänderung nicht vollständig nachziehen
|
|
958
|
+
* (Quelle weg, Pfad kein Skill mehr, Namenskonflikt), bleibt das Bundle im betroffenen Ziel auf dem
|
|
959
|
+
* alten Stand und der Lauf meldet es, statt es halb nachzuziehen (F66b).
|
|
960
|
+
*/
|
|
961
|
+
async bundleAbgleich(args) {
|
|
962
|
+
const zustand = await this.p.zustand.lesen();
|
|
963
|
+
const zielSchl = args?.ziel === undefined ? undefined : zielSchluessel(args.ziel);
|
|
964
|
+
const installationen = zustand.bundleInstallationen.filter((i) => (args?.bundle === undefined || i.bundle === args.bundle) &&
|
|
965
|
+
(zielSchl === undefined || zielSchluessel(Anwendungsfaelle.installationZiel(i)) === zielSchl));
|
|
966
|
+
const ergebnis = [];
|
|
967
|
+
for (const installation of installationen) {
|
|
968
|
+
const ziel = Anwendungsfaelle.installationZiel(installation);
|
|
969
|
+
const kopf = { ziel, bundle: installation.bundle, quelle: installation.quelle };
|
|
970
|
+
try {
|
|
971
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
972
|
+
// Die Definition liegt in der Quelle: ohne Nachziehen bliebe eine Änderung unsichtbar,
|
|
973
|
+
// dieselbe Vorsorge wie bei einer Versionseinschränkung (E33). Einmal je Lauf und Quelle,
|
|
974
|
+
// ein Fehlschlag (kein Netz) arbeitet mit dem vorhandenen Stand weiter.
|
|
975
|
+
await this.refsNachziehen(installation.quelle);
|
|
976
|
+
const bundle = await this.bundleAusQuelle(installation.bundle, installation.quelle);
|
|
977
|
+
const { funde, neue } = await this.bundleAufloesen(bundle);
|
|
978
|
+
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
979
|
+
const { konflikte } = await this.bundleKonflikte(bundle, ziel, verzeichnis, funde);
|
|
980
|
+
if (konflikte.length > 0)
|
|
981
|
+
throw new BundleNamenskonflikte(bundle.name, ziel, konflikte);
|
|
982
|
+
const neueQuellen = await this.bundleQuellenAnlegen(neue);
|
|
983
|
+
for (const fund of funde)
|
|
984
|
+
await this.linkAnlegen(harness, ziel, fund);
|
|
985
|
+
const namen = funde.map((f) => f.skill.name);
|
|
986
|
+
const entfernt = [];
|
|
987
|
+
for (const name of installation.skills) {
|
|
988
|
+
if (namen.includes(name))
|
|
989
|
+
continue;
|
|
990
|
+
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
991
|
+
entfernt.push(name);
|
|
992
|
+
}
|
|
993
|
+
await this.bundleInstallationMerken(ziel, bundle, namen);
|
|
994
|
+
const gelegt = namen.filter((n) => !installation.skills.includes(n));
|
|
995
|
+
ergebnis.push({
|
|
996
|
+
...kopf,
|
|
997
|
+
ergebnis: gelegt.length === 0 && entfernt.length === 0 ? 'unveraendert' : 'aktualisiert',
|
|
998
|
+
gelegt,
|
|
999
|
+
entfernt,
|
|
1000
|
+
neueQuellen,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
catch (fehler) {
|
|
1004
|
+
ergebnis.push({
|
|
1005
|
+
...kopf,
|
|
1006
|
+
ergebnis: 'fehlgeschlagen',
|
|
1007
|
+
gelegt: [],
|
|
1008
|
+
entfernt: [],
|
|
1009
|
+
neueQuellen: [],
|
|
1010
|
+
fehler: fehlertext(fehler),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return ergebnis;
|
|
1015
|
+
}
|
|
463
1016
|
/**
|
|
464
1017
|
* Arbeitsverzeichnis hinzufügen (E21, E26): Zeiger suchen, Bindung je vorhandenem Harness merken.
|
|
465
1018
|
* Kein Zeiger heißt: Verzeichnis merken, keine Bindung, Hinweis. Mehrdeutig heißt: Fehler,
|
|
@@ -679,28 +1232,36 @@ export class Anwendungsfaelle {
|
|
|
679
1232
|
/**
|
|
680
1233
|
* Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
|
|
681
1234
|
* einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
|
|
1235
|
+
*
|
|
1236
|
+
* Gehörte der Link zu einem Bundle, zerbricht es in diesem Ziel (E69): die übrigen Links bleiben
|
|
1237
|
+
* als Einzel-Installationen liegen, der Bundle-Verweis fällt weg. `zerbrochen` nennt die Bundles,
|
|
1238
|
+
* damit die Oberfläche es sagen kann.
|
|
682
1239
|
*/
|
|
683
1240
|
async linkEntfernen(args) {
|
|
684
1241
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
685
1242
|
// Vor jeder Wirkung: kein Entfernen, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
686
1243
|
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
687
1244
|
await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
|
|
1245
|
+
const zustand = await this.p.zustand.lesen();
|
|
1246
|
+
const zerbrochen = Anwendungsfaelle.bundlesZerbrechen(zustand, args.ziel, [args.skill]);
|
|
1247
|
+
if (zerbrochen.length > 0)
|
|
1248
|
+
await this.p.zustand.schreiben(zustand);
|
|
688
1249
|
if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
|
|
689
1250
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
690
1251
|
if (zeigerPfad !== undefined)
|
|
691
|
-
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).' };
|
|
692
|
-
return {};
|
|
1252
|
+
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).', zerbrochen };
|
|
1253
|
+
return { zerbrochen };
|
|
693
1254
|
}
|
|
694
1255
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
695
|
-
return {};
|
|
1256
|
+
return { zerbrochen };
|
|
696
1257
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
697
1258
|
if (zeigerPfad === undefined)
|
|
698
|
-
return {};
|
|
1259
|
+
return { zerbrochen };
|
|
699
1260
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
700
1261
|
if (!zeiger.skills.delete(args.skill))
|
|
701
|
-
return {};
|
|
1262
|
+
return { zerbrochen };
|
|
702
1263
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
703
|
-
return {};
|
|
1264
|
+
return { zerbrochen };
|
|
704
1265
|
}
|
|
705
1266
|
/**
|
|
706
1267
|
* Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
|
|
@@ -1041,7 +1602,14 @@ export class Anwendungsfaelle {
|
|
|
1041
1602
|
checkouts.push({
|
|
1042
1603
|
checkout: { ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) },
|
|
1043
1604
|
aktualisierung,
|
|
1605
|
+
geaenderteSkills: await this.geaenderteSkills(checkout, aktualisierung),
|
|
1044
1606
|
});
|
|
1607
|
+
// `aktualisieren` holt für einen Checkout dieselben Refs wie `refsNachziehen` (fetch auf dem
|
|
1608
|
+
// Basis-Clone). Ist das gelaufen, braucht der Bundle-Abgleich unten die Quelle nicht ein
|
|
1609
|
+
// zweites Mal zu holen. Ein übersprungener oder gescheiterter Checkout hat nichts geholt und
|
|
1610
|
+
// wird deshalb nicht vermerkt.
|
|
1611
|
+
if (aktualisierung.ergebnis === 'aktualisiert' || aktualisierung.ergebnis === 'unveraendert')
|
|
1612
|
+
this.nachgezogen.add(quelleSchluessel(checkout.quelle));
|
|
1045
1613
|
}
|
|
1046
1614
|
// Nach den Checkouts die Plugins nachziehen (E14).
|
|
1047
1615
|
const pluginHarnesses = [];
|
|
@@ -1051,7 +1619,41 @@ export class Anwendungsfaelle {
|
|
|
1051
1619
|
await harness.plugins.aktualisieren();
|
|
1052
1620
|
pluginHarnesses.push(harness.name);
|
|
1053
1621
|
}
|
|
1054
|
-
|
|
1622
|
+
// Bundles vor dem Aufräumen (E69, F64=O1): der Abgleich legt und entfernt Links, und erst
|
|
1623
|
+
// danach steht fest, in welchen Checkout noch ein Link zeigt. Ein Fehlschlag je Bundle steckt
|
|
1624
|
+
// in seinem Eintrag, er bricht den Lauf nicht ab — wie eine unerreichbare Quelle (E51).
|
|
1625
|
+
const bundles = await this.bundleAbgleich();
|
|
1626
|
+
return {
|
|
1627
|
+
checkouts,
|
|
1628
|
+
pluginHarnesses,
|
|
1629
|
+
bundles,
|
|
1630
|
+
aufraeumen: await this.verwaisteAufraeumen(checkouts, rueckfragen),
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Welche Skills ein aktualisierter Checkout mitgebracht hat (E70). Die geänderten Pfade kommen
|
|
1635
|
+
* aus dem Vergleich von altem und neuem Commit, die Skills aus dem Checkout selbst: ein Skill,
|
|
1636
|
+
* der in dieser Quelle liegt, hier aber nicht ausgecheckt ist, geht den Nutzer nichts an. Ein
|
|
1637
|
+
* Skill zählt, wenn eine geänderte Datei in seinem Ordner liegt; ein Wurzel-Skill ist das ganze
|
|
1638
|
+
* Repo und zählt bei jeder Änderung.
|
|
1639
|
+
*
|
|
1640
|
+
* Nur bei `aktualisiert` gibt es zwei Commits zum Vergleichen. Scheitert die Ermittlung, bleibt
|
|
1641
|
+
* die Liste leer: der Bericht wird ungenauer, der Lauf nicht kaputt.
|
|
1642
|
+
*/
|
|
1643
|
+
async geaenderteSkills(checkout, aktualisierung) {
|
|
1644
|
+
if (aktualisierung.ergebnis !== 'aktualisiert')
|
|
1645
|
+
return [];
|
|
1646
|
+
const dateien = await this.p.quellen
|
|
1647
|
+
.geaenderteDateien(checkout.quelle, aktualisierung.von, aktualisierung.nach)
|
|
1648
|
+
.catch(() => []);
|
|
1649
|
+
if (dateien.length === 0)
|
|
1650
|
+
return [];
|
|
1651
|
+
const skills = await this.p.quellen.ausgecheckteSkills(checkout).catch(() => []);
|
|
1652
|
+
const namen = new Set();
|
|
1653
|
+
for (const skill of skills)
|
|
1654
|
+
if (dateien.some((datei) => skill.pfad === '' || datei === skill.pfad || datei.startsWith(`${skill.pfad}/`)))
|
|
1655
|
+
namen.add(skill.name);
|
|
1656
|
+
return [...namen].sort((a, b) => a.localeCompare(b, 'de'));
|
|
1055
1657
|
}
|
|
1056
1658
|
/**
|
|
1057
1659
|
* Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
|
|
@@ -1171,8 +1773,9 @@ export class Anwendungsfaelle {
|
|
|
1171
1773
|
if (!enthalten)
|
|
1172
1774
|
return unbekannt('lokale-aenderungen');
|
|
1173
1775
|
const quelle = normalisiert(quelleRoh);
|
|
1174
|
-
// Der
|
|
1175
|
-
//
|
|
1776
|
+
// Der Katalog (skills()) kennt einen Wurzel-Skill zwar seit dem E11-Nachtrag, aber er gilt
|
|
1777
|
+
// für die geführte Quelle am gefragten Ref. Hier liegt eine fremde Arbeitskopie an Ort und
|
|
1778
|
+
// Stelle: ihr Wurzel-Skill wird direkt von der Platte gelesen, ohne Clone (E48 a).
|
|
1176
1779
|
const skill = await this.p.quellen.wurzelSkill(entryPfad).catch(() => null);
|
|
1177
1780
|
if (skill === null)
|
|
1178
1781
|
return unbekannt('skill-unbekannt');
|
|
@@ -1438,6 +2041,14 @@ export class Anwendungsfaelle {
|
|
|
1438
2041
|
for (const ei of eintraege)
|
|
1439
2042
|
if (ei.art === 'fremd')
|
|
1440
2043
|
uebernahme.set(ei.name, await this.erkennenFremd(harness, verzeichnis, ei, bekannt, true));
|
|
2044
|
+
// Bundles dieses Ziels (E69), rein aus dem Zustand: nur die Mitglieder, die dort auch
|
|
2045
|
+
// wirklich noch liegen — ein von Hand gelöschter Link soll die Gruppe nicht länger führen.
|
|
2046
|
+
const eigeneNamen = new Set(eintraege.filter((ei) => ei.art !== 'fremd').map((ei) => (ei.art === 'link' ? ei.link.name : ei.name)));
|
|
2047
|
+
const bundles = Anwendungsfaelle.installationenAn(zustand, ziel).map((i) => ({
|
|
2048
|
+
name: i.bundle,
|
|
2049
|
+
quelle: i.quelle,
|
|
2050
|
+
skills: i.skills.filter((name) => eigeneNamen.has(name)),
|
|
2051
|
+
}));
|
|
1441
2052
|
ergebnis.push({
|
|
1442
2053
|
ziel,
|
|
1443
2054
|
verzeichnis,
|
|
@@ -1446,6 +2057,7 @@ export class Anwendungsfaelle {
|
|
|
1446
2057
|
gruende: await this.gruende(harness, ziel, eintraege, zeiger, marktplatzFunde),
|
|
1447
2058
|
ignoriert,
|
|
1448
2059
|
uebernahme,
|
|
2060
|
+
bundles,
|
|
1449
2061
|
istRegistriert,
|
|
1450
2062
|
kannSkillOrdner: skillOrdnerFaehig,
|
|
1451
2063
|
...(plugins === undefined ? {} : { plugins }),
|