hmgr 0.0.3 → 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/checkout-pfad.js +5 -20
- package/dist/adapter/claude-code.js +21 -17
- package/dist/adapter/dateisystem.js +105 -5
- package/dist/adapter/git.js +124 -11
- package/dist/adapter/hermes.js +163 -10
- package/dist/adapter/quelle-text.js +1 -3
- package/dist/adapter/skill-md.js +94 -18
- package/dist/adapter/terminal.js +24 -1
- package/dist/adapter/zustand-json.js +4 -0
- package/dist/cli/hilfe.js +144 -27
- package/dist/cli/main.js +374 -28
- 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 +798 -68
- package/dist/kern/modell.js +175 -1
- package/dist/kern/quelle-ort.js +42 -0
- package/dist/ports/harness.js +13 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +5 -2
|
@@ -1,25 +1,63 @@
|
|
|
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, 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
|
+
/**
|
|
7
|
+
* Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
|
|
8
|
+
* Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
|
|
9
|
+
*/
|
|
10
|
+
export function aufraeumFrage(checkouts) {
|
|
11
|
+
const kopf = checkouts.length === 1
|
|
12
|
+
? 'Ein Checkout ist verwaist, kein Link zeigt hinein:'
|
|
13
|
+
: `${checkouts.length} Checkouts sind verwaist, kein Link zeigt hinein:`;
|
|
14
|
+
const pfade = checkouts.map((c) => ` ${c.pfad}`).join('\n');
|
|
15
|
+
return `${kopf}\n${pfade}\nLöschen?`;
|
|
16
|
+
}
|
|
6
17
|
function arbeitsverzeichnisZiel(harness, pfad) {
|
|
7
18
|
return { harness, ort: { art: 'arbeitsverzeichnis', pfad } };
|
|
8
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
|
+
}
|
|
9
24
|
function fehlertext(fehler) {
|
|
10
25
|
return fehler instanceof Error ? fehler.message : String(fehler);
|
|
11
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Quellenhinweis bei Mehrdeutigkeit (E12): Teilstring des Schlüssels oder der Schreibweise. Beides
|
|
29
|
+
* zählt, damit `host/besitzer/repo` genauso greift wie ein Stück der URL, unter der die Quelle
|
|
30
|
+
* geführt wird (E36).
|
|
31
|
+
*/
|
|
32
|
+
function quellePasst(quelle, hinweis) {
|
|
33
|
+
if (hinweis === undefined)
|
|
34
|
+
return true;
|
|
35
|
+
return quelleSchluessel(quelle).includes(hinweis) || quelleAlsText(quelle).includes(hinweis);
|
|
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
|
+
}
|
|
12
48
|
export class Anwendungsfaelle {
|
|
13
49
|
p;
|
|
14
50
|
/** Schon beantwortete Orte, je Ziel der Hinweis, der davon übrig blieb (E34). */
|
|
15
51
|
beantwortet = new Map();
|
|
52
|
+
/** Quellen, deren Refs in diesem Lauf schon nachgezogen wurden (E33). */
|
|
53
|
+
nachgezogen = new Set();
|
|
16
54
|
constructor(p) {
|
|
17
55
|
this.p = p;
|
|
18
56
|
}
|
|
19
57
|
harness(name) {
|
|
20
58
|
const h = this.p.harnesses.find((h) => h.name === name);
|
|
21
59
|
if (!h)
|
|
22
|
-
throw new
|
|
60
|
+
throw new HarnessTypUnbekannt(name);
|
|
23
61
|
return h;
|
|
24
62
|
}
|
|
25
63
|
/** Ein namentlich genannter Harness muss auf dem Rechner liegen, sonst passiert nichts Stilles. */
|
|
@@ -45,18 +83,269 @@ export class Anwendungsfaelle {
|
|
|
45
83
|
ergebnis.push(harness.name);
|
|
46
84
|
return ergebnis;
|
|
47
85
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Quelle hinzufügen: prüft, dass sie erreichbar ist, und liest Skills und Marketplace (E24).
|
|
215
|
+
* Steht dieselbe Quelle schon in einer anderen Schreibweise im Zustand, kommt keine zweite dazu
|
|
216
|
+
* (E36). Gelesen wird dann unter der Schreibweise, unter der sie geführt wird, denn mit ihr ist
|
|
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.
|
|
222
|
+
*/
|
|
223
|
+
async quelleHinzufuegen(quelle, name) {
|
|
53
224
|
const zustand = await this.p.zustand.lesen();
|
|
54
225
|
const key = quelleSchluessel(quelle);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await this.p.
|
|
226
|
+
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
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 };
|
|
58
232
|
}
|
|
59
|
-
|
|
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 };
|
|
60
349
|
}
|
|
61
350
|
/** Marketplace einer Quelle am Standard-Ref, mit den Plugins als Modellwerte. */
|
|
62
351
|
async marketplaceLesen(quelle) {
|
|
@@ -75,6 +364,31 @@ export class Anwendungsfaelle {
|
|
|
75
364
|
})),
|
|
76
365
|
};
|
|
77
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
|
+
}
|
|
78
392
|
/** Alle Marketplaces der bekannten Quellen, für `hmgr plugin list`. */
|
|
79
393
|
async marketplaces() {
|
|
80
394
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -95,7 +409,7 @@ export class Anwendungsfaelle {
|
|
|
95
409
|
const zustand = await this.p.zustand.lesen();
|
|
96
410
|
const funde = [];
|
|
97
411
|
for (const quelle of zustand.quellen) {
|
|
98
|
-
if (
|
|
412
|
+
if (!quellePasst(quelle, quelleHinweis))
|
|
99
413
|
continue;
|
|
100
414
|
const gelesen = await this.marketplaceLesen(quelle).catch(() => null);
|
|
101
415
|
if (gelesen === null)
|
|
@@ -168,7 +482,7 @@ export class Anwendungsfaelle {
|
|
|
168
482
|
if (fund.ergebnis === 'gefunden')
|
|
169
483
|
zeigerPfad = fund.pfad;
|
|
170
484
|
else if (fund.ergebnis === 'mehrdeutig')
|
|
171
|
-
|
|
485
|
+
zeigerPfad = await this.zeigerWaehlen(verzeichnis, fund.kandidaten);
|
|
172
486
|
else
|
|
173
487
|
hinweis = `Kein Zeiger unter ${verzeichnis}, alles bleibt lokal (E26).`;
|
|
174
488
|
}
|
|
@@ -177,7 +491,7 @@ export class Anwendungsfaelle {
|
|
|
177
491
|
zustand.arbeitsverzeichnisse.push(verzeichnis);
|
|
178
492
|
if (zeigerPfad !== undefined) {
|
|
179
493
|
const gebunden = zeigerPfad;
|
|
180
|
-
for (const harness of await this.
|
|
494
|
+
for (const harness of await this.vorhandeneAktiveHarnesses()) {
|
|
181
495
|
if (!harness.orte().includes('arbeitsverzeichnis'))
|
|
182
496
|
continue;
|
|
183
497
|
const ziel = arbeitsverzeichnisZiel(harness.name, verzeichnis);
|
|
@@ -201,6 +515,20 @@ export class Anwendungsfaelle {
|
|
|
201
515
|
zustand.bindungen = zustand.bindungen.filter((b) => !(b.ziel.ort.art === 'arbeitsverzeichnis' && b.ziel.ort.pfad === verzeichnis));
|
|
202
516
|
await this.p.zustand.schreiben(zustand);
|
|
203
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Mehrere composer.json unter einem Verzeichnis (E26): hmgr rät nicht, es fragt. Bleibt die Frage
|
|
520
|
+
* ohne Antwort, weil niemand antworten kann oder die Eingabe keine der Kandidaten trifft, gilt
|
|
521
|
+
* wie bisher der Fehler mit dem Weg über --composer. Der Fragetext kommt von hier, der Port kennt
|
|
522
|
+
* kein Terminal.
|
|
523
|
+
*/
|
|
524
|
+
async zeigerWaehlen(verzeichnis, kandidaten) {
|
|
525
|
+
const frage = `Mehrere composer.json unter ${verzeichnis}. Welche ist der Zeiger?`;
|
|
526
|
+
const gewaehlt = await this.p.rueckfrage.auswaehlen(frage, kandidaten);
|
|
527
|
+
const pfad = gewaehlt === undefined ? undefined : kandidaten[gewaehlt];
|
|
528
|
+
if (pfad === undefined)
|
|
529
|
+
throw new ZeigerMehrdeutig(verzeichnis, kandidaten);
|
|
530
|
+
return pfad;
|
|
531
|
+
}
|
|
204
532
|
/** Zeiger eines Arbeitsverzeichnisses aus dem Zustand, egal für welchen Harness (E26). */
|
|
205
533
|
async bindungFuerVerzeichnis(verzeichnis) {
|
|
206
534
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -212,12 +540,25 @@ export class Anwendungsfaelle {
|
|
|
212
540
|
const zustand = await this.p.zustand.lesen();
|
|
213
541
|
return zustand.bindungen.find((b) => zielSchluessel(b.ziel) === schluessel)?.zeigerPfad;
|
|
214
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
|
+
}
|
|
215
556
|
/** Skill über alle Quellen suchen. Mehrere Treffer sind nur mit Quellenhinweis erlaubt. */
|
|
216
557
|
async skillFinden(name, quelleHinweis) {
|
|
217
558
|
const zustand = await this.p.zustand.lesen();
|
|
218
559
|
const funde = [];
|
|
219
560
|
for (const quelle of zustand.quellen) {
|
|
220
|
-
if (
|
|
561
|
+
if (!quellePasst(quelle, quelleHinweis))
|
|
221
562
|
continue;
|
|
222
563
|
const ref = await this.p.quellen.standardRef(quelle);
|
|
223
564
|
const skills = await this.p.quellen.skills(quelle, ref);
|
|
@@ -279,6 +620,9 @@ export class Anwendungsfaelle {
|
|
|
279
620
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
280
621
|
if (zeigerPfad === undefined)
|
|
281
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);
|
|
282
626
|
const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
|
|
283
627
|
const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
|
|
284
628
|
await this.zeigerErgaenzen(zeigerPfad, fund, version);
|
|
@@ -287,6 +631,8 @@ export class Anwendungsfaelle {
|
|
|
287
631
|
}
|
|
288
632
|
/** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
|
|
289
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);
|
|
290
636
|
const link = await this.linkAnlegen(harness, ziel, fund, ersetzen);
|
|
291
637
|
const hinweise = [];
|
|
292
638
|
if (lokal !== true && ziel.ort.art === 'arbeitsverzeichnis') {
|
|
@@ -297,6 +643,12 @@ export class Anwendungsfaelle {
|
|
|
297
643
|
else
|
|
298
644
|
await this.zeigerErgaenzen(zeigerPfad, fund);
|
|
299
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
|
+
}
|
|
300
652
|
const vomHarness = await this.harnessHinweis(harness, ziel.ort);
|
|
301
653
|
if (vomHarness !== undefined)
|
|
302
654
|
hinweise.push(vomHarness);
|
|
@@ -324,19 +676,68 @@ export class Anwendungsfaelle {
|
|
|
324
676
|
this.beantwortet.set(schluessel, hinweis);
|
|
325
677
|
return hinweis;
|
|
326
678
|
}
|
|
327
|
-
/**
|
|
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
|
+
*/
|
|
328
683
|
async linkEntfernen(args) {
|
|
329
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);
|
|
330
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
|
+
}
|
|
331
694
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
332
|
-
return;
|
|
695
|
+
return {};
|
|
333
696
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
334
697
|
if (zeigerPfad === undefined)
|
|
335
|
-
return;
|
|
698
|
+
return {};
|
|
336
699
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
337
700
|
if (!zeiger.skills.delete(args.skill))
|
|
338
|
-
return;
|
|
701
|
+
return {};
|
|
339
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 });
|
|
340
741
|
}
|
|
341
742
|
/**
|
|
342
743
|
* Plugin an einem Ziel herstellen: mit Plugin-Fähigkeit über die CLI des Harness, sonst als
|
|
@@ -372,6 +773,8 @@ export class Anwendungsfaelle {
|
|
|
372
773
|
async pluginLegen(args) {
|
|
373
774
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
374
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);
|
|
375
778
|
const ergebnis = await this.pluginHerstellen(harness, args.ziel, fund);
|
|
376
779
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
377
780
|
return ergebnis;
|
|
@@ -422,6 +825,8 @@ export class Anwendungsfaelle {
|
|
|
422
825
|
async pluginEntfernen(args) {
|
|
423
826
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
424
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);
|
|
425
830
|
const schluessel = pluginSchluessel(fund.plugin);
|
|
426
831
|
const entfernt = [];
|
|
427
832
|
if (kannPlugins(harness)) {
|
|
@@ -457,6 +862,19 @@ export class Anwendungsfaelle {
|
|
|
457
862
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
458
863
|
return { ...ergebnis, imZeiger: true };
|
|
459
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Refs einer Quelle nachziehen, bevor eine Einschränkung gegen sie ausgewertet wird (E33). Ohne
|
|
867
|
+
* das sähe die Auswertung nur die Tags, die beim Clone schon da waren, ein später entstandener
|
|
868
|
+
* Tag bliebe unsichtbar. Einmal je Lauf und Quelle. Scheitert das Nachziehen, etwa ohne Netz,
|
|
869
|
+
* wird mit dem vorhandenen Stand weitergearbeitet.
|
|
870
|
+
*/
|
|
871
|
+
async refsNachziehen(quelle) {
|
|
872
|
+
const schluessel = quelleSchluessel(quelle);
|
|
873
|
+
if (this.nachgezogen.has(schluessel))
|
|
874
|
+
return;
|
|
875
|
+
this.nachgezogen.add(schluessel);
|
|
876
|
+
await this.p.quellen.refsNachziehen(quelle).catch(() => undefined);
|
|
877
|
+
}
|
|
460
878
|
/**
|
|
461
879
|
* Fund zu einem Skill aus dem Zeiger (E33). Die Einschränkung wird zuerst nach Composer-Regeln
|
|
462
880
|
* gegen die Tags der Quellen ausgewertet, der höchste passende Tag gewinnt. Findet sich keiner,
|
|
@@ -467,8 +885,9 @@ export class Anwendungsfaelle {
|
|
|
467
885
|
if (einschraenkung === null)
|
|
468
886
|
return await this.skillFinden(name, quelleHinweis);
|
|
469
887
|
const zustand = await this.p.zustand.lesen();
|
|
470
|
-
const quellen = zustand.quellen.filter((q) =>
|
|
888
|
+
const quellen = zustand.quellen.filter((q) => quellePasst(q, quelleHinweis));
|
|
471
889
|
for (const quelle of quellen) {
|
|
890
|
+
await this.refsNachziehen(quelle);
|
|
472
891
|
const tags = await this.p.quellen.tags(quelle).catch(() => []);
|
|
473
892
|
const ref = passenderRef(tags, einschraenkung);
|
|
474
893
|
if (ref === null)
|
|
@@ -502,14 +921,14 @@ export class Anwendungsfaelle {
|
|
|
502
921
|
if (fund.ergebnis === 'gefunden')
|
|
503
922
|
zeigerPfad = fund.pfad;
|
|
504
923
|
else if (fund.ergebnis === 'mehrdeutig')
|
|
505
|
-
|
|
924
|
+
zeigerPfad = await this.zeigerWaehlen(verzeichnis, fund.kandidaten);
|
|
506
925
|
}
|
|
507
926
|
if (zeigerPfad === undefined) {
|
|
508
927
|
ergebnis.hinweise.push(`Kein Zeiger unter ${verzeichnis}, nichts zu tun.`);
|
|
509
928
|
return ergebnis;
|
|
510
929
|
}
|
|
511
930
|
ergebnis.zeigerPfad = zeigerPfad;
|
|
512
|
-
const harnesses = (await this.
|
|
931
|
+
const harnesses = (await this.vorhandeneAktiveHarnesses()).filter((h) => h.orte().includes('arbeitsverzeichnis'));
|
|
513
932
|
if (harnesses.length === 0) {
|
|
514
933
|
ergebnis.hinweise.push('Kein Harness auf diesem Rechner, nichts zu tun (E20).');
|
|
515
934
|
return ergebnis;
|
|
@@ -520,7 +939,7 @@ export class Anwendungsfaelle {
|
|
|
520
939
|
if (zustand.quellen.some((q) => quelleSchluessel(q) === key))
|
|
521
940
|
continue;
|
|
522
941
|
zustand.quellen.push(quelle);
|
|
523
|
-
ergebnis.gelegt.push(`Quelle ${
|
|
942
|
+
ergebnis.gelegt.push(`Quelle ${quelleAlsText(quelle)}`);
|
|
524
943
|
}
|
|
525
944
|
if (!zustand.arbeitsverzeichnisse.includes(verzeichnis))
|
|
526
945
|
zustand.arbeitsverzeichnisse.push(verzeichnis);
|
|
@@ -602,11 +1021,28 @@ export class Anwendungsfaelle {
|
|
|
602
1021
|
* Aktualisieren (E14, E15): jeden Checkout unter der Wurzel auf den Stand seines Refs bringen.
|
|
603
1022
|
* Lokale Quellen haben keinen Checkout und bleiben unberührt (B11). Links zeigen auf die
|
|
604
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.
|
|
605
1028
|
*/
|
|
606
|
-
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]));
|
|
607
1032
|
const checkouts = [];
|
|
608
|
-
for (const checkout of await this.p.quellen.checkouts())
|
|
609
|
-
|
|
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
|
+
}
|
|
610
1046
|
// Nach den Checkouts die Plugins nachziehen (E14).
|
|
611
1047
|
const pluginHarnesses = [];
|
|
612
1048
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
@@ -615,17 +1051,175 @@ export class Anwendungsfaelle {
|
|
|
615
1051
|
await harness.plugins.aktualisieren();
|
|
616
1052
|
pluginHarnesses.push(harness.name);
|
|
617
1053
|
}
|
|
618
|
-
return { checkouts, pluginHarnesses };
|
|
1054
|
+
return { checkouts, pluginHarnesses, aufraeumen: await this.verwaisteAufraeumen(checkouts, rueckfragen) };
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
|
|
1058
|
+
* vorhandenen Harnesses, die Arbeitsverzeichnisse aus dem Zustand. Ein Link, dessen Quelle nicht
|
|
1059
|
+
* im Zustand steht, zeigt trotzdem in einen Checkout und hält ihn.
|
|
1060
|
+
*/
|
|
1061
|
+
async belegteCheckoutPfade() {
|
|
1062
|
+
const pfade = new Set();
|
|
1063
|
+
for (const zielZustand of await this.istZustand())
|
|
1064
|
+
for (const eintrag of zielZustand.eintraege) {
|
|
1065
|
+
if (eintrag.art === 'link')
|
|
1066
|
+
pfade.add(path.resolve(eintrag.link.checkout.pfad));
|
|
1067
|
+
else if (eintrag.art === 'link-ohne-quelle')
|
|
1068
|
+
pfade.add(path.resolve(eintrag.checkoutPfad));
|
|
1069
|
+
}
|
|
1070
|
+
return pfade;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Verwaiste Ref-Checkouts aufräumen. Verwaist heißt: kein Link in irgendeinem Ziel zeigt hinein.
|
|
1074
|
+
* Der Basis-Clone je Repo ist nie dabei, er steht in keiner Checkout-Liste (E22). Ein Checkout
|
|
1075
|
+
* mit lokaler Änderung bleibt liegen und wird nur gemeldet (E15), er kommt gar nicht erst in die
|
|
1076
|
+
* Frage. Gefragt wird einmal für alle Kandidaten (E28); wo niemand antwortet, gilt Nein und es
|
|
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.
|
|
1081
|
+
*/
|
|
1082
|
+
async verwaisteAufraeumen(checkouts, rueckfragen) {
|
|
1083
|
+
const belegt = await this.belegteCheckoutPfade();
|
|
1084
|
+
const verwaiste = [];
|
|
1085
|
+
for (const { checkout, aktualisierung } of checkouts) {
|
|
1086
|
+
if (belegt.has(path.resolve(checkout.pfad)))
|
|
1087
|
+
continue;
|
|
1088
|
+
// Übersprungen wird genau der Checkout mit lokaler Änderung, ein zweites Nachfragen erübrigt sich.
|
|
1089
|
+
const lokaleAenderung = aktualisierung.ergebnis === 'uebersprungen';
|
|
1090
|
+
verwaiste.push({ checkout, lokaleAenderung, geloescht: false });
|
|
1091
|
+
}
|
|
1092
|
+
if (!rueckfragen)
|
|
1093
|
+
return { verwaiste, gefragt: false };
|
|
1094
|
+
const kandidaten = verwaiste.filter((v) => !v.lokaleAenderung);
|
|
1095
|
+
if (kandidaten.length === 0)
|
|
1096
|
+
return { verwaiste, gefragt: false };
|
|
1097
|
+
if (!(await this.p.rueckfrage.bestaetigen(aufraeumFrage(kandidaten.map((k) => k.checkout)))))
|
|
1098
|
+
return { verwaiste, gefragt: true };
|
|
1099
|
+
for (const kandidat of kandidaten) {
|
|
1100
|
+
await this.p.quellen.entfernen(kandidat.checkout);
|
|
1101
|
+
kandidat.geloescht = true;
|
|
1102
|
+
}
|
|
1103
|
+
return { verwaiste, gefragt: true };
|
|
619
1104
|
}
|
|
620
1105
|
/**
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
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.
|
|
625
1126
|
*
|
|
626
|
-
* `
|
|
627
|
-
*
|
|
628
|
-
*
|
|
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.
|
|
1219
|
+
*
|
|
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.
|
|
629
1223
|
*/
|
|
630
1224
|
async uebernehmen(args) {
|
|
631
1225
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
@@ -633,60 +1227,98 @@ export class Anwendungsfaelle {
|
|
|
633
1227
|
throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
634
1228
|
const probelauf = args.probelauf === true;
|
|
635
1229
|
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
636
|
-
|
|
1230
|
+
// Je Identität die Quelle, unter deren Schreibweise sie geführt wird (E36).
|
|
1231
|
+
const bekannt = new Map();
|
|
1232
|
+
for (const q of (await this.p.zustand.lesen()).quellen)
|
|
1233
|
+
bekannt.set(quelleSchluessel(q), q);
|
|
637
1234
|
const eintraege = [];
|
|
638
|
-
|
|
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) {
|
|
639
1242
|
// Eigene Links sind schon fertig, sie kommen nicht in die Meldung.
|
|
640
1243
|
if (eintrag.art !== 'fremd')
|
|
641
1244
|
continue;
|
|
642
1245
|
if (args.name !== undefined && eintrag.name !== args.name)
|
|
643
1246
|
continue;
|
|
644
1247
|
const name = eintrag.name;
|
|
645
|
-
const
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
continue;
|
|
649
|
-
}
|
|
650
|
-
const kopie = await this.p.quellen.arbeitskopie(eintrag.linkZiel);
|
|
651
|
-
if (kopie === null) {
|
|
652
|
-
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 });
|
|
653
1251
|
continue;
|
|
654
1252
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
// Ohne Branch gibt es keinen Ref, auf den ein Checkout zeigen könnte (detached HEAD).
|
|
660
|
-
if (kopie.ref === null) {
|
|
661
|
-
liegenLassen('kein-branch');
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
const quelle = { art: 'git', url: kopie.url };
|
|
665
|
-
const ref = kopie.ref;
|
|
666
|
-
const skills = await this.p.quellen.skills(quelle, ref);
|
|
667
|
-
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.
|
|
668
1255
|
if (skill === undefined) {
|
|
669
|
-
|
|
1256
|
+
eintraege.push({ ergebnis: 'uebersprungen', name, grund: 'skill-unbekannt' });
|
|
670
1257
|
continue;
|
|
671
1258
|
}
|
|
672
1259
|
const quelleNeu = !bekannt.has(quelleSchluessel(quelle));
|
|
673
|
-
bekannt.
|
|
1260
|
+
bekannt.set(quelleSchluessel(quelle), quelle);
|
|
1261
|
+
const ordnerErsetzt = eintrag.linkZiel === undefined;
|
|
674
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).
|
|
675
1271
|
if (quelleNeu)
|
|
676
|
-
await this.quelleHinzufuegen(quelle);
|
|
1272
|
+
await this.quelleHinzufuegen(quelle, quelleNameFrei(quelleNameAbleiten(quelle), (await this.p.zustand.lesen()).quellen));
|
|
677
1273
|
await this.linkAusFund(harness, args.ziel, { quelle, ref, skill }, true, args.lokal);
|
|
678
1274
|
}
|
|
679
|
-
|
|
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
|
+
});
|
|
680
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
|
+
};
|
|
681
1310
|
return { ziel: args.ziel, probelauf, eintraege };
|
|
682
1311
|
}
|
|
683
|
-
/**
|
|
684
|
-
async
|
|
685
|
-
const ergebnis = new Map();
|
|
1312
|
+
/** Zeiger, an den ein Ziel gebunden ist. Ohne Bindung oder mit unlesbarer Datei: undefined. */
|
|
1313
|
+
async zeigerFuerZiel(ziel) {
|
|
686
1314
|
const zeigerPfad = await this.bindungFinden(ziel);
|
|
687
1315
|
if (zeigerPfad === undefined)
|
|
688
|
-
return
|
|
689
|
-
|
|
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();
|
|
690
1322
|
if (zeiger === undefined)
|
|
691
1323
|
return ergebnis;
|
|
692
1324
|
for (const [name, einschraenkung] of zeiger.skills)
|
|
@@ -694,9 +1326,80 @@ export class Anwendungsfaelle {
|
|
|
694
1326
|
ergebnis.set(name, einschraenkung);
|
|
695
1327
|
return ergebnis;
|
|
696
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
|
+
}
|
|
697
1378
|
/** Ist-Zustand: alle Ziele der vorhandenen Harnesses, aus dem Dateisystem gelesen. */
|
|
698
1379
|
async istZustand() {
|
|
699
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
|
+
};
|
|
700
1403
|
const ergebnis = [];
|
|
701
1404
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
702
1405
|
const ziele = [];
|
|
@@ -714,12 +1417,39 @@ export class Anwendungsfaelle {
|
|
|
714
1417
|
const plugins = kannPlugins(harness)
|
|
715
1418
|
? await harness.plugins.installierte(ziel).catch(() => [])
|
|
716
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));
|
|
717
1441
|
ergebnis.push({
|
|
718
1442
|
ziel,
|
|
719
1443
|
verzeichnis,
|
|
720
1444
|
eintraege,
|
|
721
|
-
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,
|
|
722
1451
|
...(plugins === undefined ? {} : { plugins }),
|
|
1452
|
+
...(verwalteterSkillOrdner === undefined ? {} : { verwalteterSkillOrdner }),
|
|
723
1453
|
});
|
|
724
1454
|
}
|
|
725
1455
|
}
|