hmgr 0.0.4 → 0.0.5
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/claude-code.js +21 -17
- package/dist/adapter/dateisystem.js +105 -5
- package/dist/adapter/git.js +96 -10
- package/dist/adapter/hermes.js +163 -10
- package/dist/adapter/skill-md.js +94 -18
- package/dist/adapter/terminal.js +1 -1
- package/dist/adapter/zustand-json.js +4 -0
- package/dist/cli/hilfe.js +129 -26
- package/dist/cli/main.js +268 -20
- package/dist/gui/icons.js +114 -0
- package/dist/gui/main.js +481 -0
- package/dist/gui/oeffnen.js +33 -0
- package/dist/gui/preload.cjs +55 -0
- package/dist/gui/renderer/fonts/Inter-LICENSE.txt +92 -0
- package/dist/gui/renderer/fonts/InterVariable.woff2 +0 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-Medium.woff2 +0 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-OFL.txt +93 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/dist/gui/renderer/index.html +491 -0
- package/dist/gui/renderer/renderer.js +2611 -0
- package/dist/gui/renderer/styles.css +638 -0
- package/dist/kern/anwendungsfaelle.js +688 -65
- package/dist/kern/modell.js +159 -0
- package/dist/ports/harness.js +13 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +5 -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, HarnessNichtVorhanden, KeinPassenderRef, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
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";
|
|
4
4
|
import { passenderRef } from "./version.js";
|
|
5
|
-
import { kannPlugins, kannProfile } from "../ports/harness.js";
|
|
5
|
+
import { kannIstRegistriert, 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.
|
|
@@ -12,11 +12,15 @@ export function aufraeumFrage(checkouts) {
|
|
|
12
12
|
? 'Ein Checkout ist verwaist, kein Link zeigt hinein:'
|
|
13
13
|
: `${checkouts.length} Checkouts sind verwaist, kein Link zeigt hinein:`;
|
|
14
14
|
const pfade = checkouts.map((c) => ` ${c.pfad}`).join('\n');
|
|
15
|
-
return `${kopf}\n${pfade}\nLöschen
|
|
15
|
+
return `${kopf}\n${pfade}\nLöschen?`;
|
|
16
16
|
}
|
|
17
17
|
function arbeitsverzeichnisZiel(harness, pfad) {
|
|
18
18
|
return { harness, ort: { art: 'arbeitsverzeichnis', pfad } };
|
|
19
19
|
}
|
|
20
|
+
/** Gleichheit eines ignorierten Eintrags mit einem Ziel plus Name (E43): harness, Ort strukturell, Name. */
|
|
21
|
+
function ignorierterEintragGleich(i, args) {
|
|
22
|
+
return zielSchluessel({ harness: i.harness, ort: i.ort }) === zielSchluessel(args.ziel) && i.name === args.name;
|
|
23
|
+
}
|
|
20
24
|
function fehlertext(fehler) {
|
|
21
25
|
return fehler instanceof Error ? fehler.message : String(fehler);
|
|
22
26
|
}
|
|
@@ -30,6 +34,17 @@ function quellePasst(quelle, hinweis) {
|
|
|
30
34
|
return true;
|
|
31
35
|
return quelleSchluessel(quelle).includes(hinweis) || quelleAlsText(quelle).includes(hinweis);
|
|
32
36
|
}
|
|
37
|
+
/** Ordnerpfad eines Skills relativ zur Quelle, aus `skillPath` des Lockfiles (E48 b): das `SKILL.md`-Suffix fällt weg. */
|
|
38
|
+
function skillPfadAusLock(skillPath) {
|
|
39
|
+
let pfad = skillPath.replace(/\\/g, '/');
|
|
40
|
+
if (pfad.endsWith('/SKILL.md'))
|
|
41
|
+
pfad = pfad.slice(0, -'/SKILL.md'.length);
|
|
42
|
+
else if (pfad.endsWith('SKILL.md'))
|
|
43
|
+
pfad = pfad.slice(0, -'SKILL.md'.length);
|
|
44
|
+
if (pfad.endsWith('/'))
|
|
45
|
+
pfad = pfad.slice(0, -1);
|
|
46
|
+
return pfad;
|
|
47
|
+
}
|
|
33
48
|
export class Anwendungsfaelle {
|
|
34
49
|
p;
|
|
35
50
|
/** Schon beantwortete Orte, je Ziel der Hinweis, der davon übrig blieb (E34). */
|
|
@@ -42,7 +57,7 @@ export class Anwendungsfaelle {
|
|
|
42
57
|
harness(name) {
|
|
43
58
|
const h = this.p.harnesses.find((h) => h.name === name);
|
|
44
59
|
if (!h)
|
|
45
|
-
throw new
|
|
60
|
+
throw new HarnessTypUnbekannt(name);
|
|
46
61
|
return h;
|
|
47
62
|
}
|
|
48
63
|
/** Ein namentlich genannter Harness muss auf dem Rechner liegen, sonst passiert nichts Stilles. */
|
|
@@ -68,25 +83,269 @@ export class Anwendungsfaelle {
|
|
|
68
83
|
ergebnis.push(harness.name);
|
|
69
84
|
return ergebnis;
|
|
70
85
|
}
|
|
86
|
+
/** Namen aller Harness-Typen, die hmgr kennt, aktiv oder nicht. Reine Aufzählung, ohne Regel. */
|
|
87
|
+
harnessTypen() {
|
|
88
|
+
return this.p.harnesses.map((h) => h.name);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Harness-Typen, die hmgr verwaltet (E38, GRILL.md Runde 2). Nur für sie richtet hmgr beim
|
|
92
|
+
* Arbeitsverzeichnis-Hinzufügen und beim Abgleich etwas ein. Standard: nur claude-code.
|
|
93
|
+
*/
|
|
94
|
+
async aktiveHarnesses() {
|
|
95
|
+
return (await this.p.zustand.lesen()).aktiveHarnesses;
|
|
96
|
+
}
|
|
97
|
+
/** Vorhandene Harnesses, gefiltert auf die aktiven (E38). Für Einrichten-Abläufe über alle Harnesses. */
|
|
98
|
+
async vorhandeneAktiveHarnesses() {
|
|
99
|
+
const aktiv = new Set(await this.aktiveHarnesses());
|
|
100
|
+
return (await this.vorhandeneHarnesses()).filter((h) => aktiv.has(h.name));
|
|
101
|
+
}
|
|
102
|
+
/** Aktiviert einen Harness-Typ (E38). Unbekannter Name ist ein Fehler, schon aktiv ist keiner. */
|
|
103
|
+
async harnessAktivieren(name) {
|
|
104
|
+
this.harness(name); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
105
|
+
const zustand = await this.p.zustand.lesen();
|
|
106
|
+
if (zustand.aktiveHarnesses.includes(name))
|
|
107
|
+
return;
|
|
108
|
+
zustand.aktiveHarnesses.push(name);
|
|
109
|
+
await this.p.zustand.schreiben(zustand);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Deaktiviert einen Harness-Typ (E38). Unbekannter Name ist ein Fehler, schon inaktiv ist keiner.
|
|
113
|
+
* Säubert nicht automatisch: liegende Konfiguration meldet die GUI über `istZustand`, entfernt
|
|
114
|
+
* wird sie erst mit `saeubern`.
|
|
115
|
+
*/
|
|
116
|
+
async harnessDeaktivieren(name) {
|
|
117
|
+
this.harness(name); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
118
|
+
const zustand = await this.p.zustand.lesen();
|
|
119
|
+
if (!zustand.aktiveHarnesses.includes(name))
|
|
120
|
+
return;
|
|
121
|
+
zustand.aktiveHarnesses = zustand.aktiveHarnesses.filter((h) => h !== name);
|
|
122
|
+
await this.p.zustand.schreiben(zustand);
|
|
123
|
+
}
|
|
124
|
+
/** Ein Harness mit der Fähigkeit „verwalteter Skill-Ordner" (E56), sonst `SkillOrdnerFaehigkeitFehlt`. */
|
|
125
|
+
harnessMitSkillOrdner(name) {
|
|
126
|
+
const harness = this.harness(name);
|
|
127
|
+
if (!kannSkillOrdner(harness))
|
|
128
|
+
throw new SkillOrdnerFaehigkeitFehlt(name);
|
|
129
|
+
return harness;
|
|
130
|
+
}
|
|
131
|
+
/** Der verwaltete Skill-Ordner eines Ziels (E56), oder `undefined`, wenn keiner verwaltet wird. */
|
|
132
|
+
async verwalteterSkillOrdner(ziel) {
|
|
133
|
+
await this.harnessGeprueft(ziel.harness);
|
|
134
|
+
return this.harnessMitSkillOrdner(ziel.harness).verwalteterSkillOrdner(ziel.ort);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Skill-Ordner festlegen (E56): erlaubt nur, wenn für dieses Ziel noch keiner verwaltet wird
|
|
138
|
+
* (sonst `SkillOrdnerBereitsVerwaltet`). Den genannten Pfad deutet und prüft die Fähigkeit selbst
|
|
139
|
+
* (unterhalb des Hermes-Home, nicht der eigene Ordner), sie kennt den Aufbau; der Kern reicht ihn
|
|
140
|
+
* unverändert durch und gibt den aufgelösten Pfad zurück.
|
|
141
|
+
*/
|
|
142
|
+
async skillOrdnerFestlegen(ziel, pfad) {
|
|
143
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
144
|
+
const faehig = this.harnessMitSkillOrdner(harness.name);
|
|
145
|
+
const vorhanden = await faehig.verwalteterSkillOrdner(ziel.ort);
|
|
146
|
+
if (vorhanden !== undefined)
|
|
147
|
+
throw new SkillOrdnerBereitsVerwaltet(ziel, vorhanden);
|
|
148
|
+
return await faehig.skillOrdnerFestlegen(ziel.ort, pfad);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Skill-Ordner entfernen (E56): nur der Eintrag in der Harness-Konfiguration verschwindet, der
|
|
152
|
+
* Ordner bleibt liegen. Erhebt vorher den Inhalt (hmgr-Links, fremde Einträge), wie `istZustand`
|
|
153
|
+
* ihn für dieses Ziel zeigen würde, fürs anschließende Lösch-Angebot des Aufrufers. Ohne
|
|
154
|
+
* verwalteten Eintrag: `SkillOrdnerNichtVerwaltet`.
|
|
155
|
+
*/
|
|
156
|
+
async skillOrdnerEntfernen(ziel) {
|
|
157
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
158
|
+
const faehig = this.harnessMitSkillOrdner(harness.name);
|
|
159
|
+
const pfad = await faehig.verwalteterSkillOrdner(ziel.ort);
|
|
160
|
+
if (pfad === undefined)
|
|
161
|
+
throw new SkillOrdnerNichtVerwaltet(ziel);
|
|
162
|
+
const eintraege = await this.p.ziele.eintraege(ziel, pfad).catch(() => []);
|
|
163
|
+
const hmgrLinks = eintraege.filter((e) => e.art !== 'fremd').length;
|
|
164
|
+
const fremdeEintraege = eintraege.filter((e) => e.art === 'fremd').length;
|
|
165
|
+
await faehig.skillOrdnerEntfernen(ziel.ort);
|
|
166
|
+
return { ziel, pfad, hmgrLinks, fremdeEintraege };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Den zuvor verwalteten Skill-Ordner mit Inhalt löschen (E56), ein eigener, destruktiver Schritt
|
|
170
|
+
* nach `skillOrdnerEntfernen`. Die Bestätigung liegt beim Aufrufer (CLI über den Rückfrage-Port,
|
|
171
|
+
* GUI über einen eigenen Dialog), hier nur die Ausführung samt Sicherheitsnetz in der Fähigkeit.
|
|
172
|
+
*/
|
|
173
|
+
async skillOrdnerLoeschen(ziel, pfad) {
|
|
174
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
175
|
+
await this.harnessMitSkillOrdner(harness.name).skillOrdnerLoeschen(ziel.ort, pfad);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Säubern (E38): entfernt in allen Zielen dieses Harness-Typs (Home, Profile, bekannte
|
|
179
|
+
* Arbeitsverzeichnisse) die von hmgr angelegten Links und baut harness-eigene Einträge zurück, die
|
|
180
|
+
* hmgr angelegt hat (etwa Hermes' external_dirs-Eintrag). Fremde Einträge bleiben liegen. Läuft
|
|
181
|
+
* unabhängig davon, ob der Harness aktiv ist oder auf dem Rechner liegt, damit auch eine
|
|
182
|
+
* Deinstallation sauber aufgeräumt werden kann.
|
|
183
|
+
*/
|
|
184
|
+
async saeubern(harnessName) {
|
|
185
|
+
const harness = this.harness(harnessName); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
186
|
+
const zustand = await this.p.zustand.lesen();
|
|
187
|
+
const ziele = [];
|
|
188
|
+
if (harness.orte().includes('home'))
|
|
189
|
+
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
190
|
+
if (harness.orte().includes('profil') && kannProfile(harness))
|
|
191
|
+
for (const name of await harness.profile())
|
|
192
|
+
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
193
|
+
if (harness.orte().includes('arbeitsverzeichnis'))
|
|
194
|
+
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
195
|
+
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
196
|
+
const zielErgebnisse = [];
|
|
197
|
+
for (const ziel of ziele) {
|
|
198
|
+
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
199
|
+
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
200
|
+
const entfernteLinks = [];
|
|
201
|
+
for (const eintrag of eintraege) {
|
|
202
|
+
if (eintrag.art === 'fremd')
|
|
203
|
+
continue; // fremde Einträge bleiben liegen
|
|
204
|
+
const name = eintrag.art === 'link' ? eintrag.link.name : eintrag.name;
|
|
205
|
+
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
206
|
+
entfernteLinks.push(name);
|
|
207
|
+
}
|
|
208
|
+
const konfigurationZurueckgebaut = kannZurueckbauen(harness) ? await harness.zurueckbauen(ziel.ort) : false;
|
|
209
|
+
zielErgebnisse.push({ ziel, entfernteLinks, konfigurationZurueckgebaut });
|
|
210
|
+
}
|
|
211
|
+
return { harness: harness.name, ziele: zielErgebnisse };
|
|
212
|
+
}
|
|
71
213
|
/**
|
|
72
214
|
* Quelle hinzufügen: prüft, dass sie erreichbar ist, und liest Skills und Marketplace (E24).
|
|
73
215
|
* Steht dieselbe Quelle schon in einer anderen Schreibweise im Zustand, kommt keine zweite dazu
|
|
74
216
|
* (E36). Gelesen wird dann unter der Schreibweise, unter der sie geführt wird, denn mit ihr ist
|
|
75
217
|
* der Basis-Clone angelegt. `bekannt` sagt, ob es so kam, `quelle` nennt die geführte Fassung.
|
|
218
|
+
*
|
|
219
|
+
* Anzeigename (E59): ohne `name` wird der abgeleitete Name gespeichert. Namen sind eindeutig,
|
|
220
|
+
* eine andere geführte Quelle mit demselben Anzeigenamen ist ein Fehler (`QuelleNameBelegt`).
|
|
221
|
+
* Bei einer schon bekannten Quelle wird ihr Name nicht überschrieben, `name` bleibt ohne Wirkung.
|
|
76
222
|
*/
|
|
77
|
-
async quelleHinzufuegen(quelle) {
|
|
223
|
+
async quelleHinzufuegen(quelle, name) {
|
|
78
224
|
const zustand = await this.p.zustand.lesen();
|
|
79
225
|
const key = quelleSchluessel(quelle);
|
|
80
226
|
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
zustand.quellen.push(quelle);
|
|
87
|
-
await this.p.zustand.schreiben(zustand);
|
|
227
|
+
if (gefuehrt !== undefined) {
|
|
228
|
+
const ref = await this.p.quellen.standardRef(gefuehrt);
|
|
229
|
+
const skills = await this.p.quellen.skills(gefuehrt, ref);
|
|
230
|
+
const marketplace = await this.p.quellen.marketplace(gefuehrt, ref);
|
|
231
|
+
return { quelle: gefuehrt, skills, marketplace, bekannt: true };
|
|
88
232
|
}
|
|
89
|
-
|
|
233
|
+
const angefragterName = name?.trim();
|
|
234
|
+
const effektiverName = angefragterName !== undefined && angefragterName.length > 0 ? angefragterName : quelleNameAbleiten(quelle);
|
|
235
|
+
const kollision = zustand.quellen.find((q) => quelleName(q) === effektiverName);
|
|
236
|
+
if (kollision !== undefined)
|
|
237
|
+
throw new QuelleNameBelegt(effektiverName, kollision);
|
|
238
|
+
const neu = { ...quelle, name: effektiverName };
|
|
239
|
+
const ref = await this.p.quellen.standardRef(neu);
|
|
240
|
+
const skills = await this.p.quellen.skills(neu, ref);
|
|
241
|
+
const marketplace = await this.p.quellen.marketplace(neu, ref);
|
|
242
|
+
zustand.quellen.push(neu);
|
|
243
|
+
await this.p.zustand.schreiben(zustand);
|
|
244
|
+
return { quelle: neu, skills, marketplace, bekannt: false };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Quelle umbenennen (E59): ändert nur den Anzeigenamen, die Identität bleibt `quelleSchluessel`
|
|
248
|
+
* (E36). Referenz über URL/Pfad wie `quelleEntfernen`, eine unbekannte Quelle ist ein Fehler.
|
|
249
|
+
* Namen sind eindeutig, eine Kollision mit einer anderen geführten Quelle bricht ab, ohne zu
|
|
250
|
+
* schreiben. Gibt die geführte Quelle mit dem neuen Namen zurück.
|
|
251
|
+
*/
|
|
252
|
+
async quelleUmbenennen(quelle, name) {
|
|
253
|
+
const zustand = await this.p.zustand.lesen();
|
|
254
|
+
const key = quelleSchluessel(quelle);
|
|
255
|
+
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
256
|
+
if (gefuehrt === undefined)
|
|
257
|
+
throw new QuelleUnbekannt(quelle);
|
|
258
|
+
const neuerName = name.trim();
|
|
259
|
+
// Ein leerer Name wäre kein Name: `quelleName` fiele still auf den abgeleiteten zurück, an der
|
|
260
|
+
// Eindeutigkeitsprüfung vorbei. Lieber ein Fehler als zwei Quellen mit derselben Anzeige.
|
|
261
|
+
if (neuerName.length === 0)
|
|
262
|
+
throw new Error('Der Anzeigename darf nicht leer sein.');
|
|
263
|
+
const kollision = zustand.quellen.find((q) => quelleSchluessel(q) !== key && quelleName(q) === neuerName);
|
|
264
|
+
if (kollision !== undefined)
|
|
265
|
+
throw new QuelleNameBelegt(neuerName, kollision);
|
|
266
|
+
const umbenannt = { ...gefuehrt, name: neuerName };
|
|
267
|
+
zustand.quellen = zustand.quellen.map((q) => (quelleSchluessel(q) === key ? umbenannt : q));
|
|
268
|
+
await this.p.zustand.schreiben(zustand);
|
|
269
|
+
return umbenannt;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Quelle entfernen: reines Vergessen aus dem Zustand (E54), wie beim Arbeitsverzeichnis (E52).
|
|
273
|
+
* Checkouts und Links bleiben liegen, bestehende Links gelten danach als „Link ohne Quelle"
|
|
274
|
+
* (das erkennt der Dateisystem-Adapter von selbst). Identität über `quelleSchluessel` (E36); eine
|
|
275
|
+
* unbekannte Quelle ist ein Fehler. Gibt die geführte Fassung zurück, für eine Meldung, die dieselbe
|
|
276
|
+
* Schreibweise nennt wie source list, auch wenn der Aufruf eine andere Schreibweise genannt hat.
|
|
277
|
+
*/
|
|
278
|
+
async quelleEntfernen(quelle) {
|
|
279
|
+
const zustand = await this.p.zustand.lesen();
|
|
280
|
+
const key = quelleSchluessel(quelle);
|
|
281
|
+
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
282
|
+
if (gefuehrt === undefined)
|
|
283
|
+
throw new QuelleUnbekannt(quelle);
|
|
284
|
+
zustand.quellen = zustand.quellen.filter((q) => quelleSchluessel(q) !== key);
|
|
285
|
+
await this.p.zustand.schreiben(zustand);
|
|
286
|
+
return gefuehrt;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Inventar einer Quelle für das Aufräum-Angebot nach dem Entfernen (E58): alle von hmgr gelegten
|
|
290
|
+
* Links über alle Ziele (auch die, die ein Plugin für Harnesses ohne Plugin-Fähigkeit aufgelöst
|
|
291
|
+
* hat, sie stehen als gewöhnliche Links da), alle Checkouts der Quelle, dazu nur zum Nennen:
|
|
292
|
+
* installierte Plugins aus ihrem Marketplace und Zeiger, die sie führen. Muss VOR `quelleEntfernen`
|
|
293
|
+
* laufen: danach gelten ihre Links nur noch als „Link ohne Quelle" und die Zuordnung ist weg.
|
|
294
|
+
*/
|
|
295
|
+
async quelleAufraeumInventar(quelle) {
|
|
296
|
+
const key = quelleSchluessel(quelle);
|
|
297
|
+
const zustand = await this.p.zustand.lesen();
|
|
298
|
+
const marktplatz = await this.marketplaceLesen(quelle).catch(() => null);
|
|
299
|
+
const links = [];
|
|
300
|
+
const plugins = new Set();
|
|
301
|
+
for (const zielZustand of await this.istZustand()) {
|
|
302
|
+
for (const eintrag of zielZustand.eintraege) {
|
|
303
|
+
if (eintrag.art !== 'link')
|
|
304
|
+
continue;
|
|
305
|
+
if (quelleSchluessel(eintrag.link.checkout.quelle) !== key)
|
|
306
|
+
continue;
|
|
307
|
+
links.push({ ziel: zielZustand.ziel, verzeichnis: zielZustand.verzeichnis, name: eintrag.link.name });
|
|
308
|
+
}
|
|
309
|
+
if (marktplatz === null || zielZustand.plugins === undefined)
|
|
310
|
+
continue;
|
|
311
|
+
for (const eintrag of zielZustand.plugins) {
|
|
312
|
+
if (pluginSchluesselZerlegen(eintrag).marketplace === marktplatz.name)
|
|
313
|
+
plugins.add(eintrag);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const checkouts = (await this.p.quellen.checkouts()).filter((c) => quelleSchluessel(c.quelle) === key);
|
|
317
|
+
// Lokale Änderungen (E15, F31a): vorher erhoben, damit quelleAufraeumen VOR der ersten Löschung
|
|
318
|
+
// prüfen kann, statt einen Checkout mittendrin scheitern zu lassen.
|
|
319
|
+
const checkoutsMitLokalenAenderungen = [];
|
|
320
|
+
for (const checkout of checkouts) {
|
|
321
|
+
if (await this.p.quellen.lokaleAenderungen(checkout))
|
|
322
|
+
checkoutsMitLokalenAenderungen.push(checkout);
|
|
323
|
+
}
|
|
324
|
+
// Zeiger-Einträge (E57): nur zählen/nennen, welche Bindungen die Quelle in ihrem Zeiger führen.
|
|
325
|
+
const zeigerPfade = new Set();
|
|
326
|
+
for (const bindung of zustand.bindungen) {
|
|
327
|
+
const zeiger = await this.p.zeiger.lesen(bindung.zeigerPfad).catch(() => undefined);
|
|
328
|
+
if (zeiger !== undefined && zeiger.quellen.some((q) => quelleSchluessel(q) === key))
|
|
329
|
+
zeigerPfade.add(bindung.zeigerPfad);
|
|
330
|
+
}
|
|
331
|
+
return { quelle, links, checkouts, checkoutsMitLokalenAenderungen, plugins: [...plugins], zeigerPfade: [...zeigerPfade] };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Räumt das zuvor mit `quelleAufraeumInventar` erhobene Inventar ab (E58): jeden Link direkt über
|
|
335
|
+
* den Ziele-Port entfernen, nicht über den Anwendungsfall `linkEntfernen` (der Zeiger-Arbeit macht
|
|
336
|
+
* bzw. jetzt E57 würfe), danach jeden Checkout von der Platte nehmen. Zeiger und Plugins bleiben
|
|
337
|
+
* unangetastet (E57, E58): sie waren im Inventar nur genannt. Hat ein Checkout lokale Änderungen
|
|
338
|
+
* (E15), bricht der Aufruf VOR der ersten Löschung mit `QuelleAufraeumenLokaleAenderungen` ab,
|
|
339
|
+
* kein stilles Teil-Überspringen (F31a).
|
|
340
|
+
*/
|
|
341
|
+
async quelleAufraeumen(inventar) {
|
|
342
|
+
if (inventar.checkoutsMitLokalenAenderungen.length > 0)
|
|
343
|
+
throw new QuelleAufraeumenLokaleAenderungen(inventar.checkoutsMitLokalenAenderungen);
|
|
344
|
+
for (const link of inventar.links)
|
|
345
|
+
await this.p.ziele.linkEntfernen(link.ziel, link.verzeichnis, link.name);
|
|
346
|
+
for (const checkout of inventar.checkouts)
|
|
347
|
+
await this.p.quellen.entfernen(checkout);
|
|
348
|
+
return { entfernteLinks: inventar.links.length, entfernteCheckouts: inventar.checkouts.length };
|
|
90
349
|
}
|
|
91
350
|
/** Marketplace einer Quelle am Standard-Ref, mit den Plugins als Modellwerte. */
|
|
92
351
|
async marketplaceLesen(quelle) {
|
|
@@ -105,6 +364,31 @@ export class Anwendungsfaelle {
|
|
|
105
364
|
})),
|
|
106
365
|
};
|
|
107
366
|
}
|
|
367
|
+
/** Die bekannten Quellen, ohne sie zu lesen. Für Listen, die nur die Herkunft nennen. */
|
|
368
|
+
async quellen() {
|
|
369
|
+
return (await this.p.zustand.lesen()).quellen;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Skill-Katalog aller bekannten Quellen (E40 R3): je Quelle ihre Skills, mit `beschreibung`, wo
|
|
373
|
+
* vorhanden (E39). Für einen künftigen CLI-Weg (etwa `hmgr skill list`) gedacht, heute schon von
|
|
374
|
+
* der GUI benutzt. Eine nicht erreichbare Quelle bekommt einen Fehlervermerk statt den Lauf
|
|
375
|
+
* abzubrechen, wie bei `istZustand` und `marketplaces`.
|
|
376
|
+
*/
|
|
377
|
+
async katalog() {
|
|
378
|
+
const zustand = await this.p.zustand.lesen();
|
|
379
|
+
const ergebnis = [];
|
|
380
|
+
for (const quelle of zustand.quellen) {
|
|
381
|
+
try {
|
|
382
|
+
const ref = await this.p.quellen.standardRef(quelle);
|
|
383
|
+
const skills = await this.p.quellen.skills(quelle, ref);
|
|
384
|
+
ergebnis.push({ quelle, ref, skills });
|
|
385
|
+
}
|
|
386
|
+
catch (fehler) {
|
|
387
|
+
ergebnis.push({ quelle, skills: [], fehler: fehlertext(fehler) });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return ergebnis;
|
|
391
|
+
}
|
|
108
392
|
/** Alle Marketplaces der bekannten Quellen, für `hmgr plugin list`. */
|
|
109
393
|
async marketplaces() {
|
|
110
394
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -207,7 +491,7 @@ export class Anwendungsfaelle {
|
|
|
207
491
|
zustand.arbeitsverzeichnisse.push(verzeichnis);
|
|
208
492
|
if (zeigerPfad !== undefined) {
|
|
209
493
|
const gebunden = zeigerPfad;
|
|
210
|
-
for (const harness of await this.
|
|
494
|
+
for (const harness of await this.vorhandeneAktiveHarnesses()) {
|
|
211
495
|
if (!harness.orte().includes('arbeitsverzeichnis'))
|
|
212
496
|
continue;
|
|
213
497
|
const ziel = arbeitsverzeichnisZiel(harness.name, verzeichnis);
|
|
@@ -256,6 +540,19 @@ export class Anwendungsfaelle {
|
|
|
256
540
|
const zustand = await this.p.zustand.lesen();
|
|
257
541
|
return zustand.bindungen.find((b) => zielSchluessel(b.ziel) === schluessel)?.zeigerPfad;
|
|
258
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Wächter vor jeder Wirkung, die ohne `--local` tatsächlich in einen Zeiger schreiben würde
|
|
545
|
+
* (E57): Ziel ist ein Arbeitsverzeichnis, dafür existiert eine Bindung, und `lokal` ist nicht
|
|
546
|
+
* gesetzt. Dann wirft er `ZeigerSchreibenGesperrt`, bevor irgendein Link gelegt, entfernt oder
|
|
547
|
+
* ein Plugin installiert bzw. deinstalliert wird, also nie eine halbe Wirkung. Ohne Bindung oder
|
|
548
|
+
* mit `lokal: true` bleibt das Verhalten unverändert (E26).
|
|
549
|
+
*/
|
|
550
|
+
async zeigerSchreibenPruefen(ziel, lokal) {
|
|
551
|
+
if (lokal === true || ziel.ort.art !== 'arbeitsverzeichnis')
|
|
552
|
+
return;
|
|
553
|
+
if ((await this.bindungFinden(ziel)) !== undefined)
|
|
554
|
+
throw new ZeigerSchreibenGesperrt(ziel);
|
|
555
|
+
}
|
|
259
556
|
/** Skill über alle Quellen suchen. Mehrere Treffer sind nur mit Quellenhinweis erlaubt. */
|
|
260
557
|
async skillFinden(name, quelleHinweis) {
|
|
261
558
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -323,6 +620,9 @@ export class Anwendungsfaelle {
|
|
|
323
620
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
324
621
|
if (zeigerPfad === undefined)
|
|
325
622
|
throw new EinschraenkungOhneZeiger(args.ziel, version);
|
|
623
|
+
// Eine Versionseinschränkung schreibt immer geteilt (siehe EinschraenkungOhneZeiger oben), das
|
|
624
|
+
// trifft die Sperre also ausnahmslos, solange sie steht (E57).
|
|
625
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
326
626
|
const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
|
|
327
627
|
const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
|
|
328
628
|
await this.zeigerErgaenzen(zeigerPfad, fund, version);
|
|
@@ -331,6 +631,8 @@ export class Anwendungsfaelle {
|
|
|
331
631
|
}
|
|
332
632
|
/** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
|
|
333
633
|
async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
|
|
634
|
+
// Vor jeder Wirkung: kein Link, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
635
|
+
await this.zeigerSchreibenPruefen(ziel, lokal);
|
|
334
636
|
const link = await this.linkAnlegen(harness, ziel, fund, ersetzen);
|
|
335
637
|
const hinweise = [];
|
|
336
638
|
if (lokal !== true && ziel.ort.art === 'arbeitsverzeichnis') {
|
|
@@ -341,6 +643,12 @@ export class Anwendungsfaelle {
|
|
|
341
643
|
else
|
|
342
644
|
await this.zeigerErgaenzen(zeigerPfad, fund);
|
|
343
645
|
}
|
|
646
|
+
else if (lokal === true && ziel.ort.art === 'arbeitsverzeichnis' && (await this.bindungFinden(ziel)) !== undefined) {
|
|
647
|
+
// Zeiger-Bindung vorhanden, aber ausdrücklich lokal (E57, F32): der Zeiger bleibt unangetastet,
|
|
648
|
+
// ein Hinweis sagt das, damit es niemand für einen geteilten Link hält. Ohne Skill-Namen, damit
|
|
649
|
+
// die GUI ihn nach einem Lauf über mehrere Skills einmal statt N-mal zeigen kann.
|
|
650
|
+
hinweise.push('Der Link liegt nur lokal, der Zeiger bleibt unangetastet (F32).');
|
|
651
|
+
}
|
|
344
652
|
const vomHarness = await this.harnessHinweis(harness, ziel.ort);
|
|
345
653
|
if (vomHarness !== undefined)
|
|
346
654
|
hinweise.push(vomHarness);
|
|
@@ -368,19 +676,68 @@ export class Anwendungsfaelle {
|
|
|
368
676
|
this.beantwortet.set(schluessel, hinweis);
|
|
369
677
|
return hinweis;
|
|
370
678
|
}
|
|
371
|
-
/**
|
|
679
|
+
/**
|
|
680
|
+
* Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
|
|
681
|
+
* einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
|
|
682
|
+
*/
|
|
372
683
|
async linkEntfernen(args) {
|
|
373
684
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
685
|
+
// Vor jeder Wirkung: kein Entfernen, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
686
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
374
687
|
await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
|
|
688
|
+
if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
|
|
689
|
+
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
690
|
+
if (zeigerPfad !== undefined)
|
|
691
|
+
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).' };
|
|
692
|
+
return {};
|
|
693
|
+
}
|
|
375
694
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
376
|
-
return;
|
|
695
|
+
return {};
|
|
377
696
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
378
697
|
if (zeigerPfad === undefined)
|
|
379
|
-
return;
|
|
698
|
+
return {};
|
|
380
699
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
381
700
|
if (!zeiger.skills.delete(args.skill))
|
|
382
|
-
return;
|
|
701
|
+
return {};
|
|
383
702
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
703
|
+
return {};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
|
|
707
|
+
* (strukturell, über `zielSchluessel`) und Name.
|
|
708
|
+
*/
|
|
709
|
+
async eintragIgnorieren(args) {
|
|
710
|
+
const zustand = await this.p.zustand.lesen();
|
|
711
|
+
if (zustand.ignorierteEintraege.some((i) => ignorierterEintragGleich(i, args)))
|
|
712
|
+
return;
|
|
713
|
+
zustand.ignorierteEintraege.push({ harness: args.ziel.harness, ort: args.ziel.ort, name: args.name });
|
|
714
|
+
await this.p.zustand.schreiben(zustand);
|
|
715
|
+
}
|
|
716
|
+
/** Beachtet einen zuvor ignorierten Eintrag wieder: entfernt ihn aus der Ignorier-Liste (E43). */
|
|
717
|
+
async eintragBeachten(args) {
|
|
718
|
+
const zustand = await this.p.zustand.lesen();
|
|
719
|
+
const vorher = zustand.ignorierteEintraege.length;
|
|
720
|
+
zustand.ignorierteEintraege = zustand.ignorierteEintraege.filter((i) => !ignorierterEintragGleich(i, args));
|
|
721
|
+
if (zustand.ignorierteEintraege.length === vorher)
|
|
722
|
+
return;
|
|
723
|
+
await this.p.zustand.schreiben(zustand);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Löscht einen fremden Eintrag vom Ziel auf der Platte (E46): ein Verzeichnis rekursiv, einen
|
|
727
|
+
* fremden Symlink als Link, ohne ihm zu folgen, die Arbeitskopie dahinter bleibt unangetastet.
|
|
728
|
+
* Nur Einträge der Art 'fremd', ein hmgr-eigener Link ist ein Fehler, dafür gibt es
|
|
729
|
+
* `linkEntfernen`. Ein nicht vorhandener Eintrag ist ebenso ein Fehler. War der Eintrag ignoriert
|
|
730
|
+
* (E43), wird die Marke mitentfernt, sie zeigte sonst auf Gelöschtes.
|
|
731
|
+
*/
|
|
732
|
+
async eintragLoeschen(args) {
|
|
733
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
734
|
+
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
735
|
+
const eintraege = await this.p.ziele.eintraege(args.ziel, verzeichnis);
|
|
736
|
+
const eintrag = eintraege.find((e) => (e.art === 'link' ? e.link.name : e.name) === args.name);
|
|
737
|
+
if (eintrag === undefined || eintrag.art !== 'fremd')
|
|
738
|
+
throw new FremderEintragUnbekannt(args.ziel, args.name);
|
|
739
|
+
await this.p.ziele.eintragLoeschen(args.ziel, verzeichnis, args.name);
|
|
740
|
+
await this.eintragBeachten({ ziel: args.ziel, name: args.name });
|
|
384
741
|
}
|
|
385
742
|
/**
|
|
386
743
|
* Plugin an einem Ziel herstellen: mit Plugin-Fähigkeit über die CLI des Harness, sonst als
|
|
@@ -416,6 +773,8 @@ export class Anwendungsfaelle {
|
|
|
416
773
|
async pluginLegen(args) {
|
|
417
774
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
418
775
|
const fund = await this.pluginFinden(args.plugin, args.quelle);
|
|
776
|
+
// Vor jeder Wirkung: kein Plugin, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
777
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
419
778
|
const ergebnis = await this.pluginHerstellen(harness, args.ziel, fund);
|
|
420
779
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
421
780
|
return ergebnis;
|
|
@@ -466,6 +825,8 @@ export class Anwendungsfaelle {
|
|
|
466
825
|
async pluginEntfernen(args) {
|
|
467
826
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
468
827
|
const fund = await this.pluginFinden(args.plugin);
|
|
828
|
+
// Vor jeder Wirkung: kein Entfernen, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
829
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
469
830
|
const schluessel = pluginSchluessel(fund.plugin);
|
|
470
831
|
const entfernt = [];
|
|
471
832
|
if (kannPlugins(harness)) {
|
|
@@ -567,7 +928,7 @@ export class Anwendungsfaelle {
|
|
|
567
928
|
return ergebnis;
|
|
568
929
|
}
|
|
569
930
|
ergebnis.zeigerPfad = zeigerPfad;
|
|
570
|
-
const harnesses = (await this.
|
|
931
|
+
const harnesses = (await this.vorhandeneAktiveHarnesses()).filter((h) => h.orte().includes('arbeitsverzeichnis'));
|
|
571
932
|
if (harnesses.length === 0) {
|
|
572
933
|
ergebnis.hinweise.push('Kein Harness auf diesem Rechner, nichts zu tun (E20).');
|
|
573
934
|
return ergebnis;
|
|
@@ -660,11 +1021,28 @@ export class Anwendungsfaelle {
|
|
|
660
1021
|
* Aktualisieren (E14, E15): jeden Checkout unter der Wurzel auf den Stand seines Refs bringen.
|
|
661
1022
|
* Lokale Quellen haben keinen Checkout und bleiben unberührt (B11). Links zeigen auf die
|
|
662
1023
|
* Worktree-Pfade, die dabei stehen bleiben, also müssen sie nicht erneuert werden.
|
|
1024
|
+
*
|
|
1025
|
+
* Checkouts, deren Quelle der Zustand nicht führt, bleiben ganz außen vor (E54): sie werden weder
|
|
1026
|
+
* geholt noch als verwaist zum Löschen angeboten. Eine vergessene Quelle ist vergessen, ihre
|
|
1027
|
+
* Checkouts und Links liegen unangetastet, bis die Quelle wieder hinzukommt.
|
|
663
1028
|
*/
|
|
664
|
-
async aktualisieren() {
|
|
1029
|
+
async aktualisieren(args) {
|
|
1030
|
+
const rueckfragen = args?.rueckfragen ?? true;
|
|
1031
|
+
const bekannt = new Map((await this.p.zustand.lesen()).quellen.map((q) => [quelleSchluessel(q), q]));
|
|
665
1032
|
const checkouts = [];
|
|
666
|
-
for (const checkout of await this.p.quellen.checkouts())
|
|
667
|
-
|
|
1033
|
+
for (const checkout of await this.p.quellen.checkouts()) {
|
|
1034
|
+
const gefuehrt = bekannt.get(quelleSchluessel(checkout.quelle));
|
|
1035
|
+
if (gefuehrt === undefined)
|
|
1036
|
+
continue;
|
|
1037
|
+
const aktualisierung = await this.p.quellen.aktualisieren(checkout);
|
|
1038
|
+
// Der Quellen-Port baut Checkouts von der Platte (remote.origin.url), ohne den Zustand zu
|
|
1039
|
+
// befragen: der Anzeigename fehlt dort. Für den Bericht kommt er aus der geführten Quelle
|
|
1040
|
+
// (E59), sonst zeigte eine umbenannte Quelle hier wieder ihren abgeleiteten Namen.
|
|
1041
|
+
checkouts.push({
|
|
1042
|
+
checkout: { ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) },
|
|
1043
|
+
aktualisierung,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
668
1046
|
// Nach den Checkouts die Plugins nachziehen (E14).
|
|
669
1047
|
const pluginHarnesses = [];
|
|
670
1048
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
@@ -673,7 +1051,7 @@ export class Anwendungsfaelle {
|
|
|
673
1051
|
await harness.plugins.aktualisieren();
|
|
674
1052
|
pluginHarnesses.push(harness.name);
|
|
675
1053
|
}
|
|
676
|
-
return { checkouts, pluginHarnesses, aufraeumen: await this.verwaisteAufraeumen(checkouts) };
|
|
1054
|
+
return { checkouts, pluginHarnesses, aufraeumen: await this.verwaisteAufraeumen(checkouts, rueckfragen) };
|
|
677
1055
|
}
|
|
678
1056
|
/**
|
|
679
1057
|
* Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
|
|
@@ -697,8 +1075,11 @@ export class Anwendungsfaelle {
|
|
|
697
1075
|
* mit lokaler Änderung bleibt liegen und wird nur gemeldet (E15), er kommt gar nicht erst in die
|
|
698
1076
|
* Frage. Gefragt wird einmal für alle Kandidaten (E28); wo niemand antwortet, gilt Nein und es
|
|
699
1077
|
* wird nichts gelöscht.
|
|
1078
|
+
*
|
|
1079
|
+
* Bei `rueckfragen: false` (E44, automatischer GUI-Lauf) werden Verwaiste wie sonst ermittelt und
|
|
1080
|
+
* gemeldet, aber weder gefragt noch gelöscht.
|
|
700
1081
|
*/
|
|
701
|
-
async verwaisteAufraeumen(checkouts) {
|
|
1082
|
+
async verwaisteAufraeumen(checkouts, rueckfragen) {
|
|
702
1083
|
const belegt = await this.belegteCheckoutPfade();
|
|
703
1084
|
const verwaiste = [];
|
|
704
1085
|
for (const { checkout, aktualisierung } of checkouts) {
|
|
@@ -708,6 +1089,8 @@ export class Anwendungsfaelle {
|
|
|
708
1089
|
const lokaleAenderung = aktualisierung.ergebnis === 'uebersprungen';
|
|
709
1090
|
verwaiste.push({ checkout, lokaleAenderung, geloescht: false });
|
|
710
1091
|
}
|
|
1092
|
+
if (!rueckfragen)
|
|
1093
|
+
return { verwaiste, gefragt: false };
|
|
711
1094
|
const kandidaten = verwaiste.filter((v) => !v.lokaleAenderung);
|
|
712
1095
|
if (kandidaten.length === 0)
|
|
713
1096
|
return { verwaiste, gefragt: false };
|
|
@@ -720,14 +1103,123 @@ export class Anwendungsfaelle {
|
|
|
720
1103
|
return { verwaiste, gefragt: true };
|
|
721
1104
|
}
|
|
722
1105
|
/**
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
1106
|
+
* Erkennungskette eines fremden Eintrags (E31, E48), rein lesend, ohne etwas zu verändern. Sie
|
|
1107
|
+
* bedient Prognose (`istZustand`) und Übernahme gleichermaßen:
|
|
1108
|
+
* - Ein Symlink führt in eine Arbeitskopie (B12): Quelle und Ref aus `remote.origin.url` und dem
|
|
1109
|
+
* ausgecheckten Branch, der Skill über den relativen Pfad in der Quelle.
|
|
1110
|
+
* - Sonst gilt ein Ordner mit eigenem `.git` an Ort und Stelle als Arbeitskopie (E48 a): erkannt
|
|
1111
|
+
* nur, wenn sie sauber ist und ihr HEAD im lokalen Stand des Remote-Branchs enthalten ist,
|
|
1112
|
+
* sonst „lokale-aenderungen". Erkannt daran, dass `arbeitskopie` auf den Eintrag selbst
|
|
1113
|
+
* angewandt dessen eigene Wurzel liefert (`relativerPfad === ''`).
|
|
1114
|
+
* - Sonst greift bei einem agents-Ziel das Lockfile der skills-CLI (E48 b, HARNESS-FAKTEN):
|
|
1115
|
+
* `sourceUrl` und `ref` liefern die Quelle, ersetzt wird nur, wenn der Ordnerinhalt dem
|
|
1116
|
+
* festgehaltenen `skillFolderHash` entspricht. Das Hash-Verfahren der skills-CLI ist nur für
|
|
1117
|
+
* Nicht-GitHub-Quellen als SHA-256 über Dateien reproduzierbar (`computeSkillFolderHash`);
|
|
1118
|
+
* für GitHub-Quellen benutzt sie eine Baum-SHA aus der GitHub-API (40 Zeichen statt 64), die
|
|
1119
|
+
* sich ohne Netzzugriff nicht nachbilden lässt. Ein `skillFolderHash` mit 40 Zeichen ergibt
|
|
1120
|
+
* deshalb ehrlich „nicht-pruefbar" statt fälschlich „lokale-aenderungen" (E50); nur ein
|
|
1121
|
+
* nachrechenbarer (64-stelliger) Hash, der abweicht, ergibt „lokale-aenderungen".
|
|
1122
|
+
* - Sonst „kein-link": nichts davon trifft zu.
|
|
1123
|
+
* Ein kaputter Teil (unlesbares Lockfile, Git-Fehler) bricht die Erhebung nie ab, er ergibt einen
|
|
1124
|
+
* Grund. `bekannt` normalisiert die erkannte Quelle auf die schon geführte Schreibweise (E36),
|
|
1125
|
+
* ohne sie selbst zu verändern.
|
|
1126
|
+
*
|
|
1127
|
+
* `nurLesend` trennt Prognose von Übernahme an genau einer Stelle: der Skill-Katalog einer
|
|
1128
|
+
* Quelle, die hmgr noch nicht führt, bleibt ungefragt. `skills()` legte sonst deren Basis-Clone
|
|
1129
|
+
* an, und die Erhebung würde für jeden fremden Symlink ein fremdes Repo aus dem Netz holen und
|
|
1130
|
+
* auf die Platte schreiben. Bekannte Quellen haben ihren Clone schon, sie werden wie bisher
|
|
1131
|
+
* gefragt. Die Prognose nennt dann Quelle und Ref ohne Skill (E48), „skill-unbekannt" fällt in
|
|
1132
|
+
* diesem Fall erst in der Vorschau der Übernahme auf.
|
|
1133
|
+
*/
|
|
1134
|
+
async erkennenFremd(harness, verzeichnis, eintrag, bekannt, nurLesend) {
|
|
1135
|
+
const unbekannt = (grund) => ({ art: 'unbekannt', grund });
|
|
1136
|
+
const normalisiert = (kandidat) => bekannt.get(quelleSchluessel(kandidat)) ?? kandidat;
|
|
1137
|
+
/** Skill aus dem Katalog: `null` heißt „nicht darin", `undefined` heißt „nicht gefragt". */
|
|
1138
|
+
const ausKatalog = async (quelle, ref, pfad) => {
|
|
1139
|
+
if (nurLesend && !bekannt.has(quelleSchluessel(quelle)))
|
|
1140
|
+
return undefined;
|
|
1141
|
+
const skills = await this.p.quellen.skills(quelle, ref).catch(() => []);
|
|
1142
|
+
return skills.find((s) => s.pfad === pfad) ?? null;
|
|
1143
|
+
};
|
|
1144
|
+
if (eintrag.linkZiel !== undefined) {
|
|
1145
|
+
const kopie = await this.p.quellen.arbeitskopie(eintrag.linkZiel).catch(() => null);
|
|
1146
|
+
if (kopie === null)
|
|
1147
|
+
return unbekannt('keine-arbeitskopie');
|
|
1148
|
+
if (kopie.url === null)
|
|
1149
|
+
return unbekannt('keine-url');
|
|
1150
|
+
if (kopie.ref === null)
|
|
1151
|
+
return unbekannt('kein-branch');
|
|
1152
|
+
const quelle = normalisiert({ art: 'git', url: kopie.url });
|
|
1153
|
+
const skill = await ausKatalog(quelle, kopie.ref, kopie.relativerPfad);
|
|
1154
|
+
if (skill === null)
|
|
1155
|
+
return unbekannt('skill-unbekannt');
|
|
1156
|
+
return { art: 'erkannt', quelle, ref: kopie.ref, ...(skill === undefined ? {} : { skill }) };
|
|
1157
|
+
}
|
|
1158
|
+
const entryPfad = path.join(verzeichnis, eintrag.name);
|
|
1159
|
+
// (a) Ordner mit eigenem .git an Ort und Stelle (E48).
|
|
1160
|
+
const eigeneKopie = await this.p.quellen.arbeitskopie(entryPfad).catch(() => null);
|
|
1161
|
+
if (eigeneKopie !== null && eigeneKopie.relativerPfad === '') {
|
|
1162
|
+
if (eigeneKopie.url === null)
|
|
1163
|
+
return unbekannt('keine-url');
|
|
1164
|
+
if (eigeneKopie.ref === null)
|
|
1165
|
+
return unbekannt('kein-branch');
|
|
1166
|
+
const quelleRoh = { art: 'git', url: eigeneKopie.url };
|
|
1167
|
+
const sauber = !(await this.p.quellen
|
|
1168
|
+
.lokaleAenderungen({ quelle: quelleRoh, ref: eigeneKopie.ref, pfad: entryPfad })
|
|
1169
|
+
.catch(() => true));
|
|
1170
|
+
const enthalten = sauber && (await this.p.quellen.imRemoteEnthalten(entryPfad, eigeneKopie.ref).catch(() => false));
|
|
1171
|
+
if (!enthalten)
|
|
1172
|
+
return unbekannt('lokale-aenderungen');
|
|
1173
|
+
const quelle = normalisiert(quelleRoh);
|
|
1174
|
+
// Der übliche Katalog (skills()) kennt nur Skills unter einem Unterverzeichnis (E11), ein
|
|
1175
|
+
// Wurzel-Skill der ganzen Arbeitskopie kommt darin nicht vor. Er wird direkt gelesen (E48 a).
|
|
1176
|
+
const skill = await this.p.quellen.wurzelSkill(entryPfad).catch(() => null);
|
|
1177
|
+
if (skill === null)
|
|
1178
|
+
return unbekannt('skill-unbekannt');
|
|
1179
|
+
return { art: 'erkannt', quelle, ref: eigeneKopie.ref, skill };
|
|
1180
|
+
}
|
|
1181
|
+
// (b) Lockfile der skills-CLI, nur für agents-Ziele (E48 b).
|
|
1182
|
+
if (harness.name === 'agents') {
|
|
1183
|
+
const skillsHeim = harness.zielVerzeichnis({ art: 'home' });
|
|
1184
|
+
const lockEintrag = await this.p.ziele
|
|
1185
|
+
.skillLockEintrag(skillsHeim, eintrag.name)
|
|
1186
|
+
.catch(() => undefined);
|
|
1187
|
+
if (lockEintrag !== undefined) {
|
|
1188
|
+
if (lockEintrag.ref === undefined)
|
|
1189
|
+
return unbekannt('kein-branch');
|
|
1190
|
+
if (lockEintrag.skillPath === undefined)
|
|
1191
|
+
return unbekannt('skill-unbekannt');
|
|
1192
|
+
// Eine GitHub-Baum-SHA (40 Hex-Zeichen) statt eines SHA-256 (64) lässt sich lokal nicht
|
|
1193
|
+
// nachrechnen (E50): ehrlich „nicht prüfbar" statt fälschlich „lokale Änderungen".
|
|
1194
|
+
if (lockEintrag.skillFolderHash.length === 40)
|
|
1195
|
+
return unbekannt('nicht-pruefbar');
|
|
1196
|
+
const echterHash = await this.p.ziele.ordnerHash(verzeichnis, eintrag.name).catch(() => undefined);
|
|
1197
|
+
if (echterHash === undefined || echterHash.toLowerCase() !== lockEintrag.skillFolderHash.toLowerCase())
|
|
1198
|
+
return unbekannt('lokale-aenderungen');
|
|
1199
|
+
const quelle = normalisiert({ art: 'git', url: lockEintrag.sourceUrl });
|
|
1200
|
+
const skill = await ausKatalog(quelle, lockEintrag.ref, skillPfadAusLock(lockEintrag.skillPath));
|
|
1201
|
+
if (skill === null)
|
|
1202
|
+
return unbekannt('skill-unbekannt');
|
|
1203
|
+
return { art: 'erkannt', quelle, ref: lockEintrag.ref, ...(skill === undefined ? {} : { skill }) };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return unbekannt('kein-link');
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Übernahme (B12, E31): fremde Einträge eines Ziels durch eigene Links auf einen hmgr-Checkout
|
|
1210
|
+
* derselben Quelle am selben Ref ersetzen. Eine Arbeitskopie, in die ein Symlink führt, bleibt
|
|
1211
|
+
* unberührt und wird nicht als Quelle registriert, das ist B11. Ein Ordner mit eigenem `.git`
|
|
1212
|
+
* oder ein Lockfile-Treffer wird dagegen selbst gelöscht und durch den Link ersetzt (E48 a/b). Ein
|
|
1213
|
+
* Eintrag, der nicht auflösbar ist, bleibt liegen und wird gemeldet, damit ein Lauf über viele
|
|
1214
|
+
* Einträge nicht an einem einzelnen abbricht.
|
|
1215
|
+
*
|
|
1216
|
+
* `probelauf` schreibt weder Zustand noch Checkout noch Link. Die Erkennungskette wird trotzdem
|
|
1217
|
+
* gelesen, sonst wären die Gründe nicht zu haben. Der Git-Adapter legt dabei den Basis-Clone der
|
|
1218
|
+
* Quelle an, das Ziel und der Zustand bleiben unangetastet.
|
|
727
1219
|
*
|
|
728
|
-
* `
|
|
729
|
-
*
|
|
730
|
-
*
|
|
1220
|
+
* Mit `name` (E46) betrachtet der Lauf nur diesen einen fremden Eintrag, ohne `name` bleibt es
|
|
1221
|
+
* bei allen fremden Einträgen des Ziels (CLI unverändert). Ein genannter Name, der am Ziel nicht
|
|
1222
|
+
* als fremder Eintrag liegt, ist ein Fehler, gilt für Probelauf wie echten Lauf gleichermaßen.
|
|
731
1223
|
*/
|
|
732
1224
|
async uebernehmen(args) {
|
|
733
1225
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
@@ -740,60 +1232,93 @@ export class Anwendungsfaelle {
|
|
|
740
1232
|
for (const q of (await this.p.zustand.lesen()).quellen)
|
|
741
1233
|
bekannt.set(quelleSchluessel(q), q);
|
|
742
1234
|
const eintraege = [];
|
|
743
|
-
|
|
1235
|
+
const alleEintraege = await this.p.ziele.eintraege(args.ziel, verzeichnis);
|
|
1236
|
+
if (args.name !== undefined) {
|
|
1237
|
+
const gefunden = alleEintraege.find((e) => (e.art === 'link' ? e.link.name : e.name) === args.name);
|
|
1238
|
+
if (gefunden === undefined || gefunden.art !== 'fremd')
|
|
1239
|
+
throw new FremderEintragUnbekannt(args.ziel, args.name);
|
|
1240
|
+
}
|
|
1241
|
+
for (const eintrag of alleEintraege) {
|
|
744
1242
|
// Eigene Links sind schon fertig, sie kommen nicht in die Meldung.
|
|
745
1243
|
if (eintrag.art !== 'fremd')
|
|
746
1244
|
continue;
|
|
747
1245
|
if (args.name !== undefined && eintrag.name !== args.name)
|
|
748
1246
|
continue;
|
|
749
1247
|
const name = eintrag.name;
|
|
750
|
-
const
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
continue;
|
|
754
|
-
}
|
|
755
|
-
const kopie = await this.p.quellen.arbeitskopie(eintrag.linkZiel);
|
|
756
|
-
if (kopie === null) {
|
|
757
|
-
liegenLassen('keine-arbeitskopie');
|
|
1248
|
+
const prognose = await this.erkennenFremd(harness, verzeichnis, eintrag, bekannt, false);
|
|
1249
|
+
if (prognose.art === 'unbekannt') {
|
|
1250
|
+
eintraege.push({ ergebnis: 'uebersprungen', name, grund: prognose.grund });
|
|
758
1251
|
continue;
|
|
759
1252
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
764
|
-
// Ohne Branch gibt es keinen Ref, auf den ein Checkout zeigen könnte (detached HEAD).
|
|
765
|
-
if (kopie.ref === null) {
|
|
766
|
-
liegenLassen('kein-branch');
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
769
|
-
// Kennt hmgr das Repo schon in anderer Schreibweise, zählt die geführte Fassung (E36).
|
|
770
|
-
const gefuehrt = bekannt.get(quelleSchluessel({ art: 'git', url: kopie.url }));
|
|
771
|
-
const quelle = gefuehrt ?? { art: 'git', url: kopie.url };
|
|
772
|
-
const ref = kopie.ref;
|
|
773
|
-
const skills = await this.p.quellen.skills(quelle, ref);
|
|
774
|
-
const skill = skills.find((s) => s.pfad === kopie.relativerPfad);
|
|
1253
|
+
const { quelle, ref, skill } = prognose;
|
|
1254
|
+
// Ohne `nurLesend` fragt die Kette den Katalog immer, ein fehlender Skill kommt hier nie an.
|
|
775
1255
|
if (skill === undefined) {
|
|
776
|
-
|
|
1256
|
+
eintraege.push({ ergebnis: 'uebersprungen', name, grund: 'skill-unbekannt' });
|
|
777
1257
|
continue;
|
|
778
1258
|
}
|
|
779
|
-
const quelleNeu =
|
|
1259
|
+
const quelleNeu = !bekannt.has(quelleSchluessel(quelle));
|
|
780
1260
|
bekannt.set(quelleSchluessel(quelle), quelle);
|
|
1261
|
+
const ordnerErsetzt = eintrag.linkZiel === undefined;
|
|
781
1262
|
if (!probelauf) {
|
|
1263
|
+
// Vor jeder Wirkung (E57): die Übernahme schreibt ohne `lokal` denselben Zeiger wie
|
|
1264
|
+
// `link`. Die Sperre muss vor `quelleHinzufuegen` greifen, sonst bliebe die Quelle im
|
|
1265
|
+
// Zustand stehen, während der Link nie entsteht. Ein früherer Durchlauf kann nichts
|
|
1266
|
+
// geschrieben haben: er wäre an derselben Stelle abgebrochen.
|
|
1267
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
1268
|
+
// Der abgeleitete Anzeigename kann schon vergeben sein (E59). Die Übernahme hat keinen Weg,
|
|
1269
|
+
// den Nutzer nach einem anderen zu fragen, darum weicht sie auf einen freien aus statt an
|
|
1270
|
+
// QuelleNameBelegt zu scheitern; umbenennen lässt er sich hinterher (source rename).
|
|
782
1271
|
if (quelleNeu)
|
|
783
|
-
await this.quelleHinzufuegen(quelle);
|
|
1272
|
+
await this.quelleHinzufuegen(quelle, quelleNameFrei(quelleNameAbleiten(quelle), (await this.p.zustand.lesen()).quellen));
|
|
784
1273
|
await this.linkAusFund(harness, args.ziel, { quelle, ref, skill }, true, args.lokal);
|
|
785
1274
|
}
|
|
786
|
-
|
|
1275
|
+
// Nur in der Vorschau (E50): welche gitignorierten Dateien beim Ersetzen verloren gingen.
|
|
1276
|
+
// Blockiert nichts, ist reine Sichtbarkeit vor dem echten Lauf.
|
|
1277
|
+
let gitignoriert;
|
|
1278
|
+
if (probelauf && ordnerErsetzt) {
|
|
1279
|
+
const liste = await this.p.quellen
|
|
1280
|
+
.gitignorierteDateien(path.join(verzeichnis, name))
|
|
1281
|
+
.catch(() => []);
|
|
1282
|
+
if (liste.length > 0)
|
|
1283
|
+
gitignoriert = liste;
|
|
1284
|
+
}
|
|
1285
|
+
eintraege.push({
|
|
1286
|
+
ergebnis: 'uebernommen',
|
|
1287
|
+
name,
|
|
1288
|
+
quelle,
|
|
1289
|
+
ref,
|
|
1290
|
+
skill,
|
|
1291
|
+
quelleNeu,
|
|
1292
|
+
ordnerErsetzt,
|
|
1293
|
+
...(gitignoriert === undefined ? {} : { gitignoriert }),
|
|
1294
|
+
});
|
|
787
1295
|
}
|
|
1296
|
+
// Ausdrücklich lokal an einem gebundenen Arbeitsverzeichnis (E57, F32): der Zeiger bleibt
|
|
1297
|
+
// unangetastet, ein Hinweis sagt das. Nur wenn wirklich etwas übernommen wurde und nur bei
|
|
1298
|
+
// bestehender Bindung, sonst wäre es Rauschen.
|
|
1299
|
+
if (!probelauf &&
|
|
1300
|
+
args.lokal === true &&
|
|
1301
|
+
args.ziel.ort.art === 'arbeitsverzeichnis' &&
|
|
1302
|
+
eintraege.some((e) => e.ergebnis === 'uebernommen') &&
|
|
1303
|
+
(await this.bindungFinden(args.ziel)) !== undefined)
|
|
1304
|
+
return {
|
|
1305
|
+
ziel: args.ziel,
|
|
1306
|
+
probelauf,
|
|
1307
|
+
eintraege,
|
|
1308
|
+
hinweis: 'Die übernommenen Links liegen nur lokal, der Zeiger bleibt unangetastet (F32).',
|
|
1309
|
+
};
|
|
788
1310
|
return { ziel: args.ziel, probelauf, eintraege };
|
|
789
1311
|
}
|
|
790
|
-
/**
|
|
791
|
-
async
|
|
792
|
-
const ergebnis = new Map();
|
|
1312
|
+
/** Zeiger, an den ein Ziel gebunden ist. Ohne Bindung oder mit unlesbarer Datei: undefined. */
|
|
1313
|
+
async zeigerFuerZiel(ziel) {
|
|
793
1314
|
const zeigerPfad = await this.bindungFinden(ziel);
|
|
794
1315
|
if (zeigerPfad === undefined)
|
|
795
|
-
return
|
|
796
|
-
|
|
1316
|
+
return undefined;
|
|
1317
|
+
return await this.p.zeiger.lesen(zeigerPfad).catch(() => undefined);
|
|
1318
|
+
}
|
|
1319
|
+
/** Versionseinschränkungen aus dem Zeiger eines Ziels (E33). Ohne Zeiger leer. */
|
|
1320
|
+
einschraenkungen(zeiger) {
|
|
1321
|
+
const ergebnis = new Map();
|
|
797
1322
|
if (zeiger === undefined)
|
|
798
1323
|
return ergebnis;
|
|
799
1324
|
for (const [name, einschraenkung] of zeiger.skills)
|
|
@@ -801,9 +1326,80 @@ export class Anwendungsfaelle {
|
|
|
801
1326
|
ergebnis.set(name, einschraenkung);
|
|
802
1327
|
return ergebnis;
|
|
803
1328
|
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Plugins, die für ein Ziel als Grund in Frage kommen: bei einem Arbeitsverzeichnis die des
|
|
1331
|
+
* gebundenen Zeigers, bei Home und Profil die aller Marketplaces der bekannten Quellen.
|
|
1332
|
+
*/
|
|
1333
|
+
async pluginKandidaten(ziel, zeiger, marktplatzFunde) {
|
|
1334
|
+
const alle = await marktplatzFunde();
|
|
1335
|
+
if (ziel.ort.art !== 'arbeitsverzeichnis')
|
|
1336
|
+
return alle;
|
|
1337
|
+
if (zeiger === undefined)
|
|
1338
|
+
return [];
|
|
1339
|
+
const funde = [];
|
|
1340
|
+
for (const eintrag of zeiger.plugins) {
|
|
1341
|
+
const zerlegt = pluginSchluesselZerlegen(eintrag);
|
|
1342
|
+
for (const fund of alle)
|
|
1343
|
+
if (fund.plugin.name === zerlegt.name &&
|
|
1344
|
+
(zerlegt.marketplace === undefined || fund.plugin.marketplace === zerlegt.marketplace))
|
|
1345
|
+
funde.push(fund);
|
|
1346
|
+
}
|
|
1347
|
+
return funde;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Grund je eigenem Link (E37). Priorität: Plugin vor Zeiger vor manuell. Die Plugin-Zugehörigkeit
|
|
1351
|
+
* steht vorn, weil sie die Einschränkung trägt, dass der Link nicht einzeln entfernt werden darf,
|
|
1352
|
+
* ohne das Plugin zu verletzen. Damit gilt auch ein von Hand gelegter Link, der eine flache
|
|
1353
|
+
* Skill-Liste komplettiert, als Plugin-Link. Diese Unschärfe ist bekannt und akzeptiert.
|
|
1354
|
+
* Bei einem Harness mit Plugin-Fähigkeit sind Plugin-Skills keine hmgr-Links (E16), dort entsteht
|
|
1355
|
+
* der Plugin-Grund nicht. Ist eine Quelle nicht erreichbar, fällt ihr Plugin als Kandidat aus.
|
|
1356
|
+
*/
|
|
1357
|
+
async gruende(harness, ziel, eintraege, zeiger, marktplatzFunde) {
|
|
1358
|
+
const gruende = new Map();
|
|
1359
|
+
const eigene = eintraege.filter((e) => e.art !== 'fremd').map((e) => (e.art === 'link' ? e.link.name : e.name));
|
|
1360
|
+
if (eigene.length === 0)
|
|
1361
|
+
return gruende;
|
|
1362
|
+
const eigeneNamen = new Set(eigene);
|
|
1363
|
+
if (!kannPlugins(harness))
|
|
1364
|
+
for (const fund of await this.pluginKandidaten(ziel, zeiger, marktplatzFunde)) {
|
|
1365
|
+
if (!(await this.pluginLiegt(harness, ziel, fund, eintraege).catch(() => false)))
|
|
1366
|
+
continue;
|
|
1367
|
+
const skills = await this.flacheSkillListe(fund.plugin.name, fund.quelle).catch(() => []);
|
|
1368
|
+
const schluessel = pluginSchluessel(fund.plugin);
|
|
1369
|
+
for (const skill of skills)
|
|
1370
|
+
if (eigeneNamen.has(skill.name) && !gruende.has(skill.name))
|
|
1371
|
+
gruende.set(skill.name, { art: 'plugin', plugin: schluessel });
|
|
1372
|
+
}
|
|
1373
|
+
for (const name of eigene)
|
|
1374
|
+
if (!gruende.has(name))
|
|
1375
|
+
gruende.set(name, zeiger?.skills.has(name) === true ? { art: 'zeiger' } : { art: 'manuell' });
|
|
1376
|
+
return gruende;
|
|
1377
|
+
}
|
|
804
1378
|
/** Ist-Zustand: alle Ziele der vorhandenen Harnesses, aus dem Dateisystem gelesen. */
|
|
805
1379
|
async istZustand() {
|
|
806
1380
|
const zustand = await this.p.zustand.lesen();
|
|
1381
|
+
// Je Identität die Quelle, unter deren Schreibweise sie geführt wird (E36, E48), für die
|
|
1382
|
+
// Übernahme-Prognose. Bleibt über den ganzen Lauf gleich, die Prognose verändert nichts.
|
|
1383
|
+
const bekannt = new Map();
|
|
1384
|
+
for (const q of zustand.quellen)
|
|
1385
|
+
bekannt.set(quelleSchluessel(q), q);
|
|
1386
|
+
// Marketplaces der bekannten Quellen, höchstens einmal je Lauf gelesen. Eine unerreichbare
|
|
1387
|
+
// Quelle fällt aus, statt die Erhebung abzubrechen.
|
|
1388
|
+
let gelesene;
|
|
1389
|
+
const marktplatzFunde = async () => {
|
|
1390
|
+
if (gelesene !== undefined)
|
|
1391
|
+
return gelesene;
|
|
1392
|
+
const funde = [];
|
|
1393
|
+
for (const quelle of zustand.quellen) {
|
|
1394
|
+
const marktplatz = await this.marketplaceLesen(quelle).catch(() => null);
|
|
1395
|
+
if (marktplatz === null)
|
|
1396
|
+
continue;
|
|
1397
|
+
for (const plugin of marktplatz.plugins)
|
|
1398
|
+
funde.push({ quelle, ref: marktplatz.ref, plugin });
|
|
1399
|
+
}
|
|
1400
|
+
gelesene = funde;
|
|
1401
|
+
return funde;
|
|
1402
|
+
};
|
|
807
1403
|
const ergebnis = [];
|
|
808
1404
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
809
1405
|
const ziele = [];
|
|
@@ -821,12 +1417,39 @@ export class Anwendungsfaelle {
|
|
|
821
1417
|
const plugins = kannPlugins(harness)
|
|
822
1418
|
? await harness.plugins.installierte(ziel).catch(() => [])
|
|
823
1419
|
: undefined;
|
|
1420
|
+
const zeiger = await this.zeigerFuerZiel(ziel);
|
|
1421
|
+
const istRegistriert = kannIstRegistriert(harness) ? await harness.istRegistriert(ziel.ort) : false;
|
|
1422
|
+
// Fähigkeit am Typwächter, nicht am Namen (E16), und nur an Orten, die sie kennt.
|
|
1423
|
+
let skillOrdnerFaehig = false;
|
|
1424
|
+
let verwalteterSkillOrdner;
|
|
1425
|
+
if (kannSkillOrdner(harness) && harness.skillOrdnerOrte().includes(ziel.ort.art)) {
|
|
1426
|
+
skillOrdnerFaehig = true;
|
|
1427
|
+
verwalteterSkillOrdner = await harness.verwalteterSkillOrdner(ziel.ort);
|
|
1428
|
+
}
|
|
1429
|
+
// Ignoriert (E43): nur Namen fremder Einträge, die tatsächlich noch auf der Platte liegen.
|
|
1430
|
+
// Die Ignorier-Liste selbst wird dabei nicht bereinigt.
|
|
1431
|
+
const fremdeNamen = new Set(eintraege.filter((ei) => ei.art === 'fremd').map((ei) => ei.name));
|
|
1432
|
+
const zielSchl = zielSchluessel(ziel);
|
|
1433
|
+
const ignoriert = zustand.ignorierteEintraege
|
|
1434
|
+
.filter((i) => zielSchluessel({ harness: i.harness, ort: i.ort }) === zielSchl && fremdeNamen.has(i.name))
|
|
1435
|
+
.map((i) => i.name);
|
|
1436
|
+
// Übernahme-Prognose je fremdem Eintrag (E48), dieselbe Kette wie `uebernehmen`, nur lesend.
|
|
1437
|
+
const uebernahme = new Map();
|
|
1438
|
+
for (const ei of eintraege)
|
|
1439
|
+
if (ei.art === 'fremd')
|
|
1440
|
+
uebernahme.set(ei.name, await this.erkennenFremd(harness, verzeichnis, ei, bekannt, true));
|
|
824
1441
|
ergebnis.push({
|
|
825
1442
|
ziel,
|
|
826
1443
|
verzeichnis,
|
|
827
1444
|
eintraege,
|
|
828
|
-
einschraenkungen:
|
|
1445
|
+
einschraenkungen: this.einschraenkungen(zeiger),
|
|
1446
|
+
gruende: await this.gruende(harness, ziel, eintraege, zeiger, marktplatzFunde),
|
|
1447
|
+
ignoriert,
|
|
1448
|
+
uebernahme,
|
|
1449
|
+
istRegistriert,
|
|
1450
|
+
kannSkillOrdner: skillOrdnerFaehig,
|
|
829
1451
|
...(plugins === undefined ? {} : { plugins }),
|
|
1452
|
+
...(verwalteterSkillOrdner === undefined ? {} : { verwalteterSkillOrdner }),
|
|
830
1453
|
});
|
|
831
1454
|
}
|
|
832
1455
|
}
|