hmgr 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/adapter/checkout-pfad.js +10 -2
- package/dist/adapter/claude-code.js +251 -23
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +146 -8
- package/dist/adapter/git.js +129 -17
- package/dist/adapter/hermes.js +235 -10
- package/dist/adapter/marketplace-json.js +2 -1
- package/dist/adapter/mcp-katalog.js +124 -0
- package/dist/adapter/quelle-text.js +10 -1
- package/dist/adapter/skill-md.js +96 -19
- package/dist/adapter/terminal.js +1 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +6 -1
- package/dist/cli/hilfe.js +227 -26
- package/dist/cli/main.js +362 -20
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/icons.js +114 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +823 -0
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +33 -0
- package/dist/gui/preload.cjs +70 -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 +581 -0
- package/dist/gui/renderer/renderer.js +3227 -0
- package/dist/gui/renderer/styles.css +701 -0
- package/dist/kern/anwendungsfaelle.js +859 -65
- package/dist/kern/modell.js +297 -0
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +16 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +47 -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, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
4
4
|
import { passenderRef } from "./version.js";
|
|
5
|
-
import { kannPlugins, kannProfile } from "../ports/harness.js";
|
|
5
|
+
import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, } from "../ports/harness.js";
|
|
6
6
|
/**
|
|
7
7
|
* Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
|
|
8
8
|
* Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
|
|
@@ -12,14 +12,34 @@ 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
|
}
|
|
27
|
+
/**
|
|
28
|
+
* `${VAR}`- und `${env:VAR}`-Verweise in einem MCP-Konfigblock, ohne Dubletten (E61, F39).
|
|
29
|
+
* Exportiert, damit die GUI dieselbe Prüfung für den Warn-Vermerk an der Katalog-Karte
|
|
30
|
+
* wiederverwenden kann (E61-Folgerunde), statt sie nachzubauen.
|
|
31
|
+
*/
|
|
32
|
+
export function variablenVerweise(konfig) {
|
|
33
|
+
const texte = 'command' in konfig
|
|
34
|
+
? [konfig.command, ...(konfig.args ?? []), ...Object.values(konfig.env ?? {})]
|
|
35
|
+
: [konfig.url, ...Object.values(konfig.headers ?? {})];
|
|
36
|
+
const namen = new Set();
|
|
37
|
+
const muster = /\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
38
|
+
for (const text of texte)
|
|
39
|
+
for (const treffer of text.matchAll(muster))
|
|
40
|
+
namen.add(treffer[1]);
|
|
41
|
+
return [...namen];
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* Quellenhinweis bei Mehrdeutigkeit (E12): Teilstring des Schlüssels oder der Schreibweise. Beides
|
|
25
45
|
* zählt, damit `host/besitzer/repo` genauso greift wie ein Stück der URL, unter der die Quelle
|
|
@@ -30,6 +50,17 @@ function quellePasst(quelle, hinweis) {
|
|
|
30
50
|
return true;
|
|
31
51
|
return quelleSchluessel(quelle).includes(hinweis) || quelleAlsText(quelle).includes(hinweis);
|
|
32
52
|
}
|
|
53
|
+
/** Ordnerpfad eines Skills relativ zur Quelle, aus `skillPath` des Lockfiles (E48 b): das `SKILL.md`-Suffix fällt weg. */
|
|
54
|
+
function skillPfadAusLock(skillPath) {
|
|
55
|
+
let pfad = skillPath.replace(/\\/g, '/');
|
|
56
|
+
if (pfad.endsWith('/SKILL.md'))
|
|
57
|
+
pfad = pfad.slice(0, -'/SKILL.md'.length);
|
|
58
|
+
else if (pfad.endsWith('SKILL.md'))
|
|
59
|
+
pfad = pfad.slice(0, -'SKILL.md'.length);
|
|
60
|
+
if (pfad.endsWith('/'))
|
|
61
|
+
pfad = pfad.slice(0, -1);
|
|
62
|
+
return pfad;
|
|
63
|
+
}
|
|
33
64
|
export class Anwendungsfaelle {
|
|
34
65
|
p;
|
|
35
66
|
/** Schon beantwortete Orte, je Ziel der Hinweis, der davon übrig blieb (E34). */
|
|
@@ -42,7 +73,7 @@ export class Anwendungsfaelle {
|
|
|
42
73
|
harness(name) {
|
|
43
74
|
const h = this.p.harnesses.find((h) => h.name === name);
|
|
44
75
|
if (!h)
|
|
45
|
-
throw new
|
|
76
|
+
throw new HarnessTypUnbekannt(name);
|
|
46
77
|
return h;
|
|
47
78
|
}
|
|
48
79
|
/** Ein namentlich genannter Harness muss auf dem Rechner liegen, sonst passiert nichts Stilles. */
|
|
@@ -68,25 +99,423 @@ export class Anwendungsfaelle {
|
|
|
68
99
|
ergebnis.push(harness.name);
|
|
69
100
|
return ergebnis;
|
|
70
101
|
}
|
|
102
|
+
/** Namen aller Harness-Typen, die hmgr kennt, aktiv oder nicht. Reine Aufzählung, ohne Regel. */
|
|
103
|
+
harnessTypen() {
|
|
104
|
+
return this.p.harnesses.map((h) => h.name);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Harness-Typen, die hmgr verwaltet (E38, GRILL.md Runde 2). Nur für sie richtet hmgr beim
|
|
108
|
+
* Arbeitsverzeichnis-Hinzufügen und beim Abgleich etwas ein. Standard: nur claude-code.
|
|
109
|
+
*/
|
|
110
|
+
async aktiveHarnesses() {
|
|
111
|
+
return (await this.p.zustand.lesen()).aktiveHarnesses;
|
|
112
|
+
}
|
|
113
|
+
/** Vorhandene Harnesses, gefiltert auf die aktiven (E38). Für Einrichten-Abläufe über alle Harnesses. */
|
|
114
|
+
async vorhandeneAktiveHarnesses() {
|
|
115
|
+
const aktiv = new Set(await this.aktiveHarnesses());
|
|
116
|
+
return (await this.vorhandeneHarnesses()).filter((h) => aktiv.has(h.name));
|
|
117
|
+
}
|
|
118
|
+
/** Aktiviert einen Harness-Typ (E38). Unbekannter Name ist ein Fehler, schon aktiv ist keiner. */
|
|
119
|
+
async harnessAktivieren(name) {
|
|
120
|
+
this.harness(name); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
121
|
+
const zustand = await this.p.zustand.lesen();
|
|
122
|
+
if (zustand.aktiveHarnesses.includes(name))
|
|
123
|
+
return;
|
|
124
|
+
zustand.aktiveHarnesses.push(name);
|
|
125
|
+
await this.p.zustand.schreiben(zustand);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Deaktiviert einen Harness-Typ (E38). Unbekannter Name ist ein Fehler, schon inaktiv ist keiner.
|
|
129
|
+
* Säubert nicht automatisch: liegende Konfiguration meldet die GUI über `istZustand`, entfernt
|
|
130
|
+
* wird sie erst mit `saeubern`.
|
|
131
|
+
*/
|
|
132
|
+
async harnessDeaktivieren(name) {
|
|
133
|
+
this.harness(name); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
134
|
+
const zustand = await this.p.zustand.lesen();
|
|
135
|
+
if (!zustand.aktiveHarnesses.includes(name))
|
|
136
|
+
return;
|
|
137
|
+
zustand.aktiveHarnesses = zustand.aktiveHarnesses.filter((h) => h !== name);
|
|
138
|
+
await this.p.zustand.schreiben(zustand);
|
|
139
|
+
}
|
|
140
|
+
/** Ein Harness mit der Fähigkeit „verwalteter Skill-Ordner" (E56), sonst `SkillOrdnerFaehigkeitFehlt`. */
|
|
141
|
+
harnessMitSkillOrdner(name) {
|
|
142
|
+
const harness = this.harness(name);
|
|
143
|
+
if (!kannSkillOrdner(harness))
|
|
144
|
+
throw new SkillOrdnerFaehigkeitFehlt(name);
|
|
145
|
+
return harness;
|
|
146
|
+
}
|
|
147
|
+
/** Der verwaltete Skill-Ordner eines Ziels (E56), oder `undefined`, wenn keiner verwaltet wird. */
|
|
148
|
+
async verwalteterSkillOrdner(ziel) {
|
|
149
|
+
await this.harnessGeprueft(ziel.harness);
|
|
150
|
+
return this.harnessMitSkillOrdner(ziel.harness).verwalteterSkillOrdner(ziel.ort);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Skill-Ordner festlegen (E56): erlaubt nur, wenn für dieses Ziel noch keiner verwaltet wird
|
|
154
|
+
* (sonst `SkillOrdnerBereitsVerwaltet`). Den genannten Pfad deutet und prüft die Fähigkeit selbst
|
|
155
|
+
* (unterhalb des Hermes-Home, nicht der eigene Ordner), sie kennt den Aufbau; der Kern reicht ihn
|
|
156
|
+
* unverändert durch und gibt den aufgelösten Pfad zurück.
|
|
157
|
+
*/
|
|
158
|
+
async skillOrdnerFestlegen(ziel, pfad) {
|
|
159
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
160
|
+
const faehig = this.harnessMitSkillOrdner(harness.name);
|
|
161
|
+
const vorhanden = await faehig.verwalteterSkillOrdner(ziel.ort);
|
|
162
|
+
if (vorhanden !== undefined)
|
|
163
|
+
throw new SkillOrdnerBereitsVerwaltet(ziel, vorhanden);
|
|
164
|
+
return await faehig.skillOrdnerFestlegen(ziel.ort, pfad);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Skill-Ordner entfernen (E56): nur der Eintrag in der Harness-Konfiguration verschwindet, der
|
|
168
|
+
* Ordner bleibt liegen. Erhebt vorher den Inhalt (hmgr-Links, fremde Einträge), wie `istZustand`
|
|
169
|
+
* ihn für dieses Ziel zeigen würde, fürs anschließende Lösch-Angebot des Aufrufers. Ohne
|
|
170
|
+
* verwalteten Eintrag: `SkillOrdnerNichtVerwaltet`.
|
|
171
|
+
*/
|
|
172
|
+
async skillOrdnerEntfernen(ziel) {
|
|
173
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
174
|
+
const faehig = this.harnessMitSkillOrdner(harness.name);
|
|
175
|
+
const pfad = await faehig.verwalteterSkillOrdner(ziel.ort);
|
|
176
|
+
if (pfad === undefined)
|
|
177
|
+
throw new SkillOrdnerNichtVerwaltet(ziel);
|
|
178
|
+
const eintraege = await this.p.ziele.eintraege(ziel, pfad).catch(() => []);
|
|
179
|
+
const hmgrLinks = eintraege.filter((e) => e.art !== 'fremd').length;
|
|
180
|
+
const fremdeEintraege = eintraege.filter((e) => e.art === 'fremd').length;
|
|
181
|
+
await faehig.skillOrdnerEntfernen(ziel.ort);
|
|
182
|
+
return { ziel, pfad, hmgrLinks, fremdeEintraege };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Den zuvor verwalteten Skill-Ordner mit Inhalt löschen (E56), ein eigener, destruktiver Schritt
|
|
186
|
+
* nach `skillOrdnerEntfernen`. Die Bestätigung liegt beim Aufrufer (CLI über den Rückfrage-Port,
|
|
187
|
+
* GUI über einen eigenen Dialog), hier nur die Ausführung samt Sicherheitsnetz in der Fähigkeit.
|
|
188
|
+
*/
|
|
189
|
+
async skillOrdnerLoeschen(ziel, pfad) {
|
|
190
|
+
const harness = await this.harnessGeprueft(ziel.harness);
|
|
191
|
+
await this.harnessMitSkillOrdner(harness.name).skillOrdnerLoeschen(ziel.ort, pfad);
|
|
192
|
+
}
|
|
193
|
+
/** Ein Harness mit der Fähigkeit MCP-Server (E61), sonst `McpFaehigkeitFehlt`. */
|
|
194
|
+
harnessMitMcp(name) {
|
|
195
|
+
const harness = this.harness(name);
|
|
196
|
+
if (!kannMcp(harness))
|
|
197
|
+
throw new McpFaehigkeitFehlt(name);
|
|
198
|
+
return harness;
|
|
199
|
+
}
|
|
200
|
+
/** MCP-Katalog, unverändert wie in ~/.hmgr/mcp.yaml gepflegt (E61). hmgr schreibt ihn nie. */
|
|
201
|
+
async mcpKatalog() {
|
|
202
|
+
return await this.p.mcpKatalog.lesen();
|
|
203
|
+
}
|
|
204
|
+
async mcpEintragFinden(mcpName) {
|
|
205
|
+
const eintrag = (await this.p.mcpKatalog.lesen()).find((e) => e.name === mcpName);
|
|
206
|
+
if (eintrag === undefined)
|
|
207
|
+
throw new McpUnbekannt(mcpName);
|
|
208
|
+
return eintrag;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* F39: fehlende `${VAR}`-Verweise als Warnung, nie als Abbruch. Geprüft wird gegen die eigene
|
|
212
|
+
* Prozess-Umgebung und, wenn die Fähigkeit eine `.env`-Datei nennt (Hermes), zusätzlich gegen sie.
|
|
213
|
+
*/
|
|
214
|
+
async mcpVariablenWarnung(faehig, eintrag, ort) {
|
|
215
|
+
const verweise = variablenVerweise(eintrag.konfig);
|
|
216
|
+
if (verweise.length === 0)
|
|
217
|
+
return undefined;
|
|
218
|
+
const prozessEnv = this.p.umgebung.variablen();
|
|
219
|
+
const envPfad = faehig.mcp.envDatei?.(ort);
|
|
220
|
+
const zusatzEnv = envPfad === undefined ? {} : await this.p.umgebung.envDatei(envPfad);
|
|
221
|
+
const fehlend = verweise.filter((v) => !(v in prozessEnv) && !(v in zusatzEnv));
|
|
222
|
+
if (fehlend.length === 0)
|
|
223
|
+
return undefined;
|
|
224
|
+
const mehrzahl = fehlend.length > 1;
|
|
225
|
+
return (`hmgr kann nicht wissen, was die Shell beim Start des Harness setzt: ` +
|
|
226
|
+
`${mehrzahl ? 'Variablen' : 'Variable'} ${fehlend.join(', ')} ${mehrzahl ? 'sind' : 'ist'} weder in der ` +
|
|
227
|
+
'aktuellen Umgebung noch in einer bekannten .env-Datei zu finden.');
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* MCP einhängen (E61): Katalog-Eintrag an ein Ziel legen, Projekt oder global. Die Fähigkeit hängt
|
|
231
|
+
* am Adapter (E16): ein Harness ohne MCP-Fähigkeit oder ohne diese Ebene ist ein Fehler. Ein
|
|
232
|
+
* fehlender `${VAR}`-Verweis bricht nicht ab, sondern kommt als Warnung zurück (F39).
|
|
233
|
+
*/
|
|
234
|
+
async mcpEinhaengen(args) {
|
|
235
|
+
const eintrag = await this.mcpEintragFinden(args.name);
|
|
236
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
237
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
238
|
+
if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
|
|
239
|
+
throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
240
|
+
await faehig.mcp.einhaengen(eintrag.name, eintrag.konfig, args.ziel.ort);
|
|
241
|
+
const warnung = await this.mcpVariablenWarnung(faehig, eintrag, args.ziel.ort);
|
|
242
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
243
|
+
return {
|
|
244
|
+
eintrag,
|
|
245
|
+
ziel: args.ziel,
|
|
246
|
+
...(hinweis === undefined ? {} : { hinweis }),
|
|
247
|
+
...(warnung === undefined ? {} : { warnung }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** MCP aushängen (E61): Gegenstück zu `mcpEinhaengen`, ohne F39-Prüfung. */
|
|
251
|
+
async mcpAushaengen(args) {
|
|
252
|
+
const eintrag = await this.mcpEintragFinden(args.name);
|
|
253
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
254
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
255
|
+
if (!faehig.mcp.ebenen().includes(args.ziel.ort.art))
|
|
256
|
+
throw new McpEbeneNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
257
|
+
await faehig.mcp.aushaengen(eintrag.name, args.ziel.ort);
|
|
258
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
259
|
+
return { eintrag, ziel: args.ziel, ...(hinweis === undefined ? {} : { hinweis }) };
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* MCP übernehmen (E64, F48=O2): einen Eintrag aus der geteilten Konfigurationsdatei eines Ortes
|
|
263
|
+
* (bei Claude Code die `.mcp.json` der Projektwurzel) in den privaten Bereich desselben Ortes
|
|
264
|
+
* holen — eintragen und aus der geteilten Datei entfernen. Der Konfigblock kommt 1:1 aus der
|
|
265
|
+
* Datei, nicht aus dem Katalog: es gibt Einträge, die der Katalog nicht kennt, und beide Sorten
|
|
266
|
+
* müssen gehen. Die Fähigkeit hängt am Adapter (E16), nicht am Namen: fehlt sie, kennt dieser
|
|
267
|
+
* Harness-Typ keine geteilte Datei.
|
|
268
|
+
*/
|
|
269
|
+
async mcpUebernehmen(args) {
|
|
270
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
271
|
+
const faehig = this.harnessMitMcp(harness.name);
|
|
272
|
+
const uebernehmen = faehig.mcp.geteiltenUebernehmen;
|
|
273
|
+
if (uebernehmen === undefined)
|
|
274
|
+
throw new McpUebernahmeNichtMoeglich(args.ziel);
|
|
275
|
+
const { datei } = await uebernehmen.call(faehig.mcp, args.name, args.ziel.ort);
|
|
276
|
+
const hinweis = faehig.mcp.hinweisNachAenderung?.();
|
|
277
|
+
return { name: args.name, ziel: args.ziel, datei, ...(hinweis === undefined ? {} : { hinweis }) };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Alle Orte, an denen MCP-Server grundsätzlich hängen können (E61): je Harness mit der Fähigkeit
|
|
281
|
+
* (E16, am Adapter abgefragt, nie am Namen) seine Ebenen aufgelöst zu tatsächlichen Zielen — Home,
|
|
282
|
+
* jedes bekannte Profil, jedes bekannte Arbeitsverzeichnis. Unabhängig davon, ob dort schon etwas
|
|
283
|
+
* hängt oder der Harness gerade aktiv ist (E38): das entscheidet der Aufrufer. Geteilt zwischen
|
|
284
|
+
* `mcpUebersicht` (dort mit `eingehaengte` abgeglichen) und `mcpPlaetze` (roh für den
|
|
285
|
+
* Einhängen-Dialog der GUI).
|
|
286
|
+
*/
|
|
287
|
+
async mcpZiele() {
|
|
288
|
+
const ziele = [];
|
|
289
|
+
const zustand = await this.p.zustand.lesen();
|
|
290
|
+
for (const harness of await this.vorhandeneHarnesses()) {
|
|
291
|
+
if (!kannMcp(harness))
|
|
292
|
+
continue;
|
|
293
|
+
const ebenen = harness.mcp.ebenen();
|
|
294
|
+
if (ebenen.includes('home'))
|
|
295
|
+
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
296
|
+
if (ebenen.includes('profil') && kannProfile(harness))
|
|
297
|
+
for (const name of await harness.profile())
|
|
298
|
+
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
299
|
+
if (ebenen.includes('arbeitsverzeichnis'))
|
|
300
|
+
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
301
|
+
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
302
|
+
}
|
|
303
|
+
return ziele;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Die Plätze für den Einhängen-Dialog der GUI (E61-Folgerunde, F42): dieselbe Liste wie in
|
|
307
|
+
* `mcpUebersicht`, aber roh, ohne den Abgleich mit Katalog-Einträgen. Die Fähigkeit hängt am
|
|
308
|
+
* Adapter, nicht am Harness-Namen (E16): agents kommt nie vor, weil es `mcp` nicht implementiert.
|
|
309
|
+
*/
|
|
310
|
+
async mcpPlaetze() {
|
|
311
|
+
return this.mcpZiele();
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Übersicht für `hmgr mcp list` (E61): je Katalog-Eintrag, wo er eingehängt ist, dazu Einträge in
|
|
315
|
+
* Harness-Konfigurationen, die nicht im Katalog stehen (nur genannt, nicht angefasst).
|
|
316
|
+
*/
|
|
317
|
+
async mcpUebersicht() {
|
|
318
|
+
const katalog = (await this.p.mcpKatalog.lesen()).map((eintrag) => ({
|
|
319
|
+
eintrag,
|
|
320
|
+
eingehaengt: [],
|
|
321
|
+
geteilteFundorte: [],
|
|
322
|
+
}));
|
|
323
|
+
const nachName = new Map(katalog.map((k) => [k.eintrag.name, k]));
|
|
324
|
+
const unbekannt = [];
|
|
325
|
+
const unbekannteGeteilteFundorte = [];
|
|
326
|
+
for (const ziel of await this.mcpZiele()) {
|
|
327
|
+
const faehig = this.harnessMitMcp(ziel.harness);
|
|
328
|
+
const namen = await faehig.mcp.eingehaengte(ziel.ort).catch(() => []);
|
|
329
|
+
for (const mcpName of namen) {
|
|
330
|
+
const eintrag = nachName.get(mcpName);
|
|
331
|
+
if (eintrag !== undefined)
|
|
332
|
+
eintrag.eingehaengt.push(ziel);
|
|
333
|
+
else
|
|
334
|
+
unbekannt.push({ ziel, name: mcpName });
|
|
335
|
+
}
|
|
336
|
+
const geteilt = (await faehig.mcp.geteilteFundorte?.(ziel.ort).catch(() => [])) ?? [];
|
|
337
|
+
for (const fundort of geteilt) {
|
|
338
|
+
const eintrag = nachName.get(fundort.name);
|
|
339
|
+
if (eintrag !== undefined)
|
|
340
|
+
eintrag.geteilteFundorte.push({ ziel, datei: fundort.datei, scope: fundort.scope });
|
|
341
|
+
else
|
|
342
|
+
unbekannteGeteilteFundorte.push({ ziel, name: fundort.name, datei: fundort.datei, scope: fundort.scope });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { katalog, unbekannt, unbekannteGeteilteFundorte };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Säubern (E38): entfernt in allen Zielen dieses Harness-Typs (Home, Profile, bekannte
|
|
349
|
+
* Arbeitsverzeichnisse) die von hmgr angelegten Links und baut harness-eigene Einträge zurück, die
|
|
350
|
+
* hmgr angelegt hat (etwa Hermes' external_dirs-Eintrag). Fremde Einträge bleiben liegen. Läuft
|
|
351
|
+
* unabhängig davon, ob der Harness aktiv ist oder auf dem Rechner liegt, damit auch eine
|
|
352
|
+
* Deinstallation sauber aufgeräumt werden kann.
|
|
353
|
+
*/
|
|
354
|
+
async saeubern(harnessName) {
|
|
355
|
+
const harness = this.harness(harnessName); // wirft HarnessTypUnbekannt bei unbekanntem Namen
|
|
356
|
+
const zustand = await this.p.zustand.lesen();
|
|
357
|
+
const ziele = [];
|
|
358
|
+
if (harness.orte().includes('home'))
|
|
359
|
+
ziele.push({ harness: harness.name, ort: { art: 'home' } });
|
|
360
|
+
if (harness.orte().includes('profil') && kannProfile(harness))
|
|
361
|
+
for (const name of await harness.profile())
|
|
362
|
+
ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
|
|
363
|
+
if (harness.orte().includes('arbeitsverzeichnis'))
|
|
364
|
+
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
365
|
+
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
366
|
+
const zielErgebnisse = [];
|
|
367
|
+
for (const ziel of ziele) {
|
|
368
|
+
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
369
|
+
const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
|
|
370
|
+
const entfernteLinks = [];
|
|
371
|
+
for (const eintrag of eintraege) {
|
|
372
|
+
if (eintrag.art === 'fremd')
|
|
373
|
+
continue; // fremde Einträge bleiben liegen
|
|
374
|
+
const name = eintrag.art === 'link' ? eintrag.link.name : eintrag.name;
|
|
375
|
+
await this.p.ziele.linkEntfernen(ziel, verzeichnis, name);
|
|
376
|
+
entfernteLinks.push(name);
|
|
377
|
+
}
|
|
378
|
+
const konfigurationZurueckgebaut = kannZurueckbauen(harness) ? await harness.zurueckbauen(ziel.ort) : false;
|
|
379
|
+
zielErgebnisse.push({ ziel, entfernteLinks, konfigurationZurueckgebaut });
|
|
380
|
+
}
|
|
381
|
+
return { harness: harness.name, ziele: zielErgebnisse };
|
|
382
|
+
}
|
|
71
383
|
/**
|
|
72
384
|
* Quelle hinzufügen: prüft, dass sie erreichbar ist, und liest Skills und Marketplace (E24).
|
|
73
385
|
* Steht dieselbe Quelle schon in einer anderen Schreibweise im Zustand, kommt keine zweite dazu
|
|
74
386
|
* (E36). Gelesen wird dann unter der Schreibweise, unter der sie geführt wird, denn mit ihr ist
|
|
75
387
|
* der Basis-Clone angelegt. `bekannt` sagt, ob es so kam, `quelle` nennt die geführte Fassung.
|
|
388
|
+
*
|
|
389
|
+
* Anzeigename (E59): ohne `name` wird der abgeleitete Name gespeichert. Namen sind eindeutig,
|
|
390
|
+
* eine andere geführte Quelle mit demselben Anzeigenamen ist ein Fehler (`QuelleNameBelegt`).
|
|
391
|
+
* Bei einer schon bekannten Quelle wird ihr Name nicht überschrieben, `name` bleibt ohne Wirkung.
|
|
76
392
|
*/
|
|
77
|
-
async quelleHinzufuegen(quelle) {
|
|
393
|
+
async quelleHinzufuegen(quelle, name) {
|
|
78
394
|
const zustand = await this.p.zustand.lesen();
|
|
79
395
|
const key = quelleSchluessel(quelle);
|
|
80
396
|
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
397
|
+
if (gefuehrt !== undefined) {
|
|
398
|
+
const ref = await this.p.quellen.standardRef(gefuehrt);
|
|
399
|
+
const skills = await this.p.quellen.skills(gefuehrt, ref);
|
|
400
|
+
const marketplace = await this.p.quellen.marketplace(gefuehrt, ref);
|
|
401
|
+
return { quelle: gefuehrt, skills, marketplace, bekannt: true };
|
|
402
|
+
}
|
|
403
|
+
const angefragterName = name?.trim();
|
|
404
|
+
const effektiverName = angefragterName !== undefined && angefragterName.length > 0 ? angefragterName : quelleNameAbleiten(quelle);
|
|
405
|
+
const kollision = zustand.quellen.find((q) => quelleName(q) === effektiverName);
|
|
406
|
+
if (kollision !== undefined)
|
|
407
|
+
throw new QuelleNameBelegt(effektiverName, kollision);
|
|
408
|
+
const neu = { ...quelle, name: effektiverName };
|
|
409
|
+
const ref = await this.p.quellen.standardRef(neu);
|
|
410
|
+
const skills = await this.p.quellen.skills(neu, ref);
|
|
411
|
+
const marketplace = await this.p.quellen.marketplace(neu, ref);
|
|
412
|
+
zustand.quellen.push(neu);
|
|
413
|
+
await this.p.zustand.schreiben(zustand);
|
|
414
|
+
return { quelle: neu, skills, marketplace, bekannt: false };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Quelle umbenennen (E59): ändert nur den Anzeigenamen, die Identität bleibt `quelleSchluessel`
|
|
418
|
+
* (E36). Referenz über URL/Pfad wie `quelleEntfernen`, eine unbekannte Quelle ist ein Fehler.
|
|
419
|
+
* Namen sind eindeutig, eine Kollision mit einer anderen geführten Quelle bricht ab, ohne zu
|
|
420
|
+
* schreiben. Gibt die geführte Quelle mit dem neuen Namen zurück.
|
|
421
|
+
*/
|
|
422
|
+
async quelleUmbenennen(quelle, name) {
|
|
423
|
+
const zustand = await this.p.zustand.lesen();
|
|
424
|
+
const key = quelleSchluessel(quelle);
|
|
425
|
+
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
426
|
+
if (gefuehrt === undefined)
|
|
427
|
+
throw new QuelleUnbekannt(quelle);
|
|
428
|
+
const neuerName = name.trim();
|
|
429
|
+
// Ein leerer Name wäre kein Name: `quelleName` fiele still auf den abgeleiteten zurück, an der
|
|
430
|
+
// Eindeutigkeitsprüfung vorbei. Lieber ein Fehler als zwei Quellen mit derselben Anzeige.
|
|
431
|
+
if (neuerName.length === 0)
|
|
432
|
+
throw new Error('Der Anzeigename darf nicht leer sein.');
|
|
433
|
+
const kollision = zustand.quellen.find((q) => quelleSchluessel(q) !== key && quelleName(q) === neuerName);
|
|
434
|
+
if (kollision !== undefined)
|
|
435
|
+
throw new QuelleNameBelegt(neuerName, kollision);
|
|
436
|
+
const umbenannt = { ...gefuehrt, name: neuerName };
|
|
437
|
+
zustand.quellen = zustand.quellen.map((q) => (quelleSchluessel(q) === key ? umbenannt : q));
|
|
438
|
+
await this.p.zustand.schreiben(zustand);
|
|
439
|
+
return umbenannt;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Quelle entfernen: reines Vergessen aus dem Zustand (E54), wie beim Arbeitsverzeichnis (E52).
|
|
443
|
+
* Checkouts und Links bleiben liegen, bestehende Links gelten danach als „Link ohne Quelle"
|
|
444
|
+
* (das erkennt der Dateisystem-Adapter von selbst). Identität über `quelleSchluessel` (E36); eine
|
|
445
|
+
* unbekannte Quelle ist ein Fehler. Gibt die geführte Fassung zurück, für eine Meldung, die dieselbe
|
|
446
|
+
* Schreibweise nennt wie source list, auch wenn der Aufruf eine andere Schreibweise genannt hat.
|
|
447
|
+
*/
|
|
448
|
+
async quelleEntfernen(quelle) {
|
|
449
|
+
const zustand = await this.p.zustand.lesen();
|
|
450
|
+
const key = quelleSchluessel(quelle);
|
|
451
|
+
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
452
|
+
if (gefuehrt === undefined)
|
|
453
|
+
throw new QuelleUnbekannt(quelle);
|
|
454
|
+
zustand.quellen = zustand.quellen.filter((q) => quelleSchluessel(q) !== key);
|
|
455
|
+
await this.p.zustand.schreiben(zustand);
|
|
456
|
+
return gefuehrt;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Inventar einer Quelle für das Aufräum-Angebot nach dem Entfernen (E58): alle von hmgr gelegten
|
|
460
|
+
* Links über alle Ziele (auch die, die ein Plugin für Harnesses ohne Plugin-Fähigkeit aufgelöst
|
|
461
|
+
* hat, sie stehen als gewöhnliche Links da), alle Checkouts der Quelle, dazu nur zum Nennen:
|
|
462
|
+
* installierte Plugins aus ihrem Marketplace und Zeiger, die sie führen. Muss VOR `quelleEntfernen`
|
|
463
|
+
* laufen: danach gelten ihre Links nur noch als „Link ohne Quelle" und die Zuordnung ist weg.
|
|
464
|
+
*/
|
|
465
|
+
async quelleAufraeumInventar(quelle) {
|
|
466
|
+
const key = quelleSchluessel(quelle);
|
|
467
|
+
const zustand = await this.p.zustand.lesen();
|
|
468
|
+
const marktplatz = await this.marketplaceLesen(quelle).catch(() => null);
|
|
469
|
+
const links = [];
|
|
470
|
+
const plugins = new Set();
|
|
471
|
+
for (const zielZustand of await this.istZustand()) {
|
|
472
|
+
for (const eintrag of zielZustand.eintraege) {
|
|
473
|
+
if (eintrag.art !== 'link')
|
|
474
|
+
continue;
|
|
475
|
+
if (quelleSchluessel(eintrag.link.checkout.quelle) !== key)
|
|
476
|
+
continue;
|
|
477
|
+
links.push({ ziel: zielZustand.ziel, verzeichnis: zielZustand.verzeichnis, name: eintrag.link.name });
|
|
478
|
+
}
|
|
479
|
+
if (marktplatz === null || zielZustand.plugins === undefined)
|
|
480
|
+
continue;
|
|
481
|
+
for (const eintrag of zielZustand.plugins) {
|
|
482
|
+
if (pluginSchluesselZerlegen(eintrag).marketplace === marktplatz.name)
|
|
483
|
+
plugins.add(eintrag);
|
|
484
|
+
}
|
|
88
485
|
}
|
|
89
|
-
|
|
486
|
+
const checkouts = (await this.p.quellen.checkouts()).filter((c) => quelleSchluessel(c.quelle) === key);
|
|
487
|
+
// Lokale Änderungen (E15, F31a): vorher erhoben, damit quelleAufraeumen VOR der ersten Löschung
|
|
488
|
+
// prüfen kann, statt einen Checkout mittendrin scheitern zu lassen.
|
|
489
|
+
const checkoutsMitLokalenAenderungen = [];
|
|
490
|
+
for (const checkout of checkouts) {
|
|
491
|
+
if (await this.p.quellen.lokaleAenderungen(checkout))
|
|
492
|
+
checkoutsMitLokalenAenderungen.push(checkout);
|
|
493
|
+
}
|
|
494
|
+
// Zeiger-Einträge (E57): nur zählen/nennen, welche Bindungen die Quelle in ihrem Zeiger führen.
|
|
495
|
+
const zeigerPfade = new Set();
|
|
496
|
+
for (const bindung of zustand.bindungen) {
|
|
497
|
+
const zeiger = await this.p.zeiger.lesen(bindung.zeigerPfad).catch(() => undefined);
|
|
498
|
+
if (zeiger !== undefined && zeiger.quellen.some((q) => quelleSchluessel(q) === key))
|
|
499
|
+
zeigerPfade.add(bindung.zeigerPfad);
|
|
500
|
+
}
|
|
501
|
+
return { quelle, links, checkouts, checkoutsMitLokalenAenderungen, plugins: [...plugins], zeigerPfade: [...zeigerPfade] };
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Räumt das zuvor mit `quelleAufraeumInventar` erhobene Inventar ab (E58): jeden Link direkt über
|
|
505
|
+
* den Ziele-Port entfernen, nicht über den Anwendungsfall `linkEntfernen` (der Zeiger-Arbeit macht
|
|
506
|
+
* bzw. jetzt E57 würfe), danach jeden Checkout von der Platte nehmen. Zeiger und Plugins bleiben
|
|
507
|
+
* unangetastet (E57, E58): sie waren im Inventar nur genannt. Hat ein Checkout lokale Änderungen
|
|
508
|
+
* (E15), bricht der Aufruf VOR der ersten Löschung mit `QuelleAufraeumenLokaleAenderungen` ab,
|
|
509
|
+
* kein stilles Teil-Überspringen (F31a).
|
|
510
|
+
*/
|
|
511
|
+
async quelleAufraeumen(inventar) {
|
|
512
|
+
if (inventar.checkoutsMitLokalenAenderungen.length > 0)
|
|
513
|
+
throw new QuelleAufraeumenLokaleAenderungen(inventar.checkoutsMitLokalenAenderungen);
|
|
514
|
+
for (const link of inventar.links)
|
|
515
|
+
await this.p.ziele.linkEntfernen(link.ziel, link.verzeichnis, link.name);
|
|
516
|
+
for (const checkout of inventar.checkouts)
|
|
517
|
+
await this.p.quellen.entfernen(checkout);
|
|
518
|
+
return { entfernteLinks: inventar.links.length, entfernteCheckouts: inventar.checkouts.length };
|
|
90
519
|
}
|
|
91
520
|
/** Marketplace einer Quelle am Standard-Ref, mit den Plugins als Modellwerte. */
|
|
92
521
|
async marketplaceLesen(quelle) {
|
|
@@ -105,6 +534,31 @@ export class Anwendungsfaelle {
|
|
|
105
534
|
})),
|
|
106
535
|
};
|
|
107
536
|
}
|
|
537
|
+
/** Die bekannten Quellen, ohne sie zu lesen. Für Listen, die nur die Herkunft nennen. */
|
|
538
|
+
async quellen() {
|
|
539
|
+
return (await this.p.zustand.lesen()).quellen;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Skill-Katalog aller bekannten Quellen (E40 R3): je Quelle ihre Skills, mit `beschreibung`, wo
|
|
543
|
+
* vorhanden (E39). Für einen künftigen CLI-Weg (etwa `hmgr skill list`) gedacht, heute schon von
|
|
544
|
+
* der GUI benutzt. Eine nicht erreichbare Quelle bekommt einen Fehlervermerk statt den Lauf
|
|
545
|
+
* abzubrechen, wie bei `istZustand` und `marketplaces`.
|
|
546
|
+
*/
|
|
547
|
+
async katalog() {
|
|
548
|
+
const zustand = await this.p.zustand.lesen();
|
|
549
|
+
const ergebnis = [];
|
|
550
|
+
for (const quelle of zustand.quellen) {
|
|
551
|
+
try {
|
|
552
|
+
const ref = await this.p.quellen.standardRef(quelle);
|
|
553
|
+
const skills = await this.p.quellen.skills(quelle, ref);
|
|
554
|
+
ergebnis.push({ quelle, ref, skills });
|
|
555
|
+
}
|
|
556
|
+
catch (fehler) {
|
|
557
|
+
ergebnis.push({ quelle, skills: [], fehler: fehlertext(fehler) });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return ergebnis;
|
|
561
|
+
}
|
|
108
562
|
/** Alle Marketplaces der bekannten Quellen, für `hmgr plugin list`. */
|
|
109
563
|
async marketplaces() {
|
|
110
564
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -207,7 +661,7 @@ export class Anwendungsfaelle {
|
|
|
207
661
|
zustand.arbeitsverzeichnisse.push(verzeichnis);
|
|
208
662
|
if (zeigerPfad !== undefined) {
|
|
209
663
|
const gebunden = zeigerPfad;
|
|
210
|
-
for (const harness of await this.
|
|
664
|
+
for (const harness of await this.vorhandeneAktiveHarnesses()) {
|
|
211
665
|
if (!harness.orte().includes('arbeitsverzeichnis'))
|
|
212
666
|
continue;
|
|
213
667
|
const ziel = arbeitsverzeichnisZiel(harness.name, verzeichnis);
|
|
@@ -256,6 +710,19 @@ export class Anwendungsfaelle {
|
|
|
256
710
|
const zustand = await this.p.zustand.lesen();
|
|
257
711
|
return zustand.bindungen.find((b) => zielSchluessel(b.ziel) === schluessel)?.zeigerPfad;
|
|
258
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Wächter vor jeder Wirkung, die ohne `--local` tatsächlich in einen Zeiger schreiben würde
|
|
715
|
+
* (E57): Ziel ist ein Arbeitsverzeichnis, dafür existiert eine Bindung, und `lokal` ist nicht
|
|
716
|
+
* gesetzt. Dann wirft er `ZeigerSchreibenGesperrt`, bevor irgendein Link gelegt, entfernt oder
|
|
717
|
+
* ein Plugin installiert bzw. deinstalliert wird, also nie eine halbe Wirkung. Ohne Bindung oder
|
|
718
|
+
* mit `lokal: true` bleibt das Verhalten unverändert (E26).
|
|
719
|
+
*/
|
|
720
|
+
async zeigerSchreibenPruefen(ziel, lokal) {
|
|
721
|
+
if (lokal === true || ziel.ort.art !== 'arbeitsverzeichnis')
|
|
722
|
+
return;
|
|
723
|
+
if ((await this.bindungFinden(ziel)) !== undefined)
|
|
724
|
+
throw new ZeigerSchreibenGesperrt(ziel);
|
|
725
|
+
}
|
|
259
726
|
/** Skill über alle Quellen suchen. Mehrere Treffer sind nur mit Quellenhinweis erlaubt. */
|
|
260
727
|
async skillFinden(name, quelleHinweis) {
|
|
261
728
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -323,6 +790,9 @@ export class Anwendungsfaelle {
|
|
|
323
790
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
324
791
|
if (zeigerPfad === undefined)
|
|
325
792
|
throw new EinschraenkungOhneZeiger(args.ziel, version);
|
|
793
|
+
// Eine Versionseinschränkung schreibt immer geteilt (siehe EinschraenkungOhneZeiger oben), das
|
|
794
|
+
// trifft die Sperre also ausnahmslos, solange sie steht (E57).
|
|
795
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
326
796
|
const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
|
|
327
797
|
const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
|
|
328
798
|
await this.zeigerErgaenzen(zeigerPfad, fund, version);
|
|
@@ -331,6 +801,8 @@ export class Anwendungsfaelle {
|
|
|
331
801
|
}
|
|
332
802
|
/** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
|
|
333
803
|
async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
|
|
804
|
+
// Vor jeder Wirkung: kein Link, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
805
|
+
await this.zeigerSchreibenPruefen(ziel, lokal);
|
|
334
806
|
const link = await this.linkAnlegen(harness, ziel, fund, ersetzen);
|
|
335
807
|
const hinweise = [];
|
|
336
808
|
if (lokal !== true && ziel.ort.art === 'arbeitsverzeichnis') {
|
|
@@ -341,6 +813,12 @@ export class Anwendungsfaelle {
|
|
|
341
813
|
else
|
|
342
814
|
await this.zeigerErgaenzen(zeigerPfad, fund);
|
|
343
815
|
}
|
|
816
|
+
else if (lokal === true && ziel.ort.art === 'arbeitsverzeichnis' && (await this.bindungFinden(ziel)) !== undefined) {
|
|
817
|
+
// Zeiger-Bindung vorhanden, aber ausdrücklich lokal (E57, F32): der Zeiger bleibt unangetastet,
|
|
818
|
+
// ein Hinweis sagt das, damit es niemand für einen geteilten Link hält. Ohne Skill-Namen, damit
|
|
819
|
+
// die GUI ihn nach einem Lauf über mehrere Skills einmal statt N-mal zeigen kann.
|
|
820
|
+
hinweise.push('Der Link liegt nur lokal, der Zeiger bleibt unangetastet (F32).');
|
|
821
|
+
}
|
|
344
822
|
const vomHarness = await this.harnessHinweis(harness, ziel.ort);
|
|
345
823
|
if (vomHarness !== undefined)
|
|
346
824
|
hinweise.push(vomHarness);
|
|
@@ -368,19 +846,68 @@ export class Anwendungsfaelle {
|
|
|
368
846
|
this.beantwortet.set(schluessel, hinweis);
|
|
369
847
|
return hinweis;
|
|
370
848
|
}
|
|
371
|
-
/**
|
|
849
|
+
/**
|
|
850
|
+
* Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). Mit `lokal` an
|
|
851
|
+
* einem gebundenen Ziel bleibt der Zeiger unangetastet, ein Hinweis sagt das (E57, F32).
|
|
852
|
+
*/
|
|
372
853
|
async linkEntfernen(args) {
|
|
373
854
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
855
|
+
// Vor jeder Wirkung: kein Entfernen, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
856
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
374
857
|
await this.p.ziele.linkEntfernen(args.ziel, harness.zielVerzeichnis(args.ziel.ort), args.skill);
|
|
858
|
+
if (args.lokal === true && args.ziel.ort.art === 'arbeitsverzeichnis') {
|
|
859
|
+
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
860
|
+
if (zeigerPfad !== undefined)
|
|
861
|
+
return { hinweis: 'Der Link ist nur lokal entfernt, der Zeiger bleibt unangetastet (F32).' };
|
|
862
|
+
return {};
|
|
863
|
+
}
|
|
375
864
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
376
|
-
return;
|
|
865
|
+
return {};
|
|
377
866
|
const zeigerPfad = await this.bindungFinden(args.ziel);
|
|
378
867
|
if (zeigerPfad === undefined)
|
|
379
|
-
return;
|
|
868
|
+
return {};
|
|
380
869
|
const zeiger = await this.p.zeiger.lesen(zeigerPfad);
|
|
381
870
|
if (!zeiger.skills.delete(args.skill))
|
|
382
|
-
return;
|
|
871
|
+
return {};
|
|
383
872
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
873
|
+
return {};
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Ignoriert einen fremden Eintrag (E43): idempotent, kein Duplikat. Gleichheit über harness, Ort
|
|
877
|
+
* (strukturell, über `zielSchluessel`) und Name.
|
|
878
|
+
*/
|
|
879
|
+
async eintragIgnorieren(args) {
|
|
880
|
+
const zustand = await this.p.zustand.lesen();
|
|
881
|
+
if (zustand.ignorierteEintraege.some((i) => ignorierterEintragGleich(i, args)))
|
|
882
|
+
return;
|
|
883
|
+
zustand.ignorierteEintraege.push({ harness: args.ziel.harness, ort: args.ziel.ort, name: args.name });
|
|
884
|
+
await this.p.zustand.schreiben(zustand);
|
|
885
|
+
}
|
|
886
|
+
/** Beachtet einen zuvor ignorierten Eintrag wieder: entfernt ihn aus der Ignorier-Liste (E43). */
|
|
887
|
+
async eintragBeachten(args) {
|
|
888
|
+
const zustand = await this.p.zustand.lesen();
|
|
889
|
+
const vorher = zustand.ignorierteEintraege.length;
|
|
890
|
+
zustand.ignorierteEintraege = zustand.ignorierteEintraege.filter((i) => !ignorierterEintragGleich(i, args));
|
|
891
|
+
if (zustand.ignorierteEintraege.length === vorher)
|
|
892
|
+
return;
|
|
893
|
+
await this.p.zustand.schreiben(zustand);
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Löscht einen fremden Eintrag vom Ziel auf der Platte (E46): ein Verzeichnis rekursiv, einen
|
|
897
|
+
* fremden Symlink als Link, ohne ihm zu folgen, die Arbeitskopie dahinter bleibt unangetastet.
|
|
898
|
+
* Nur Einträge der Art 'fremd', ein hmgr-eigener Link ist ein Fehler, dafür gibt es
|
|
899
|
+
* `linkEntfernen`. Ein nicht vorhandener Eintrag ist ebenso ein Fehler. War der Eintrag ignoriert
|
|
900
|
+
* (E43), wird die Marke mitentfernt, sie zeigte sonst auf Gelöschtes.
|
|
901
|
+
*/
|
|
902
|
+
async eintragLoeschen(args) {
|
|
903
|
+
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
904
|
+
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
905
|
+
const eintraege = await this.p.ziele.eintraege(args.ziel, verzeichnis);
|
|
906
|
+
const eintrag = eintraege.find((e) => (e.art === 'link' ? e.link.name : e.name) === args.name);
|
|
907
|
+
if (eintrag === undefined || eintrag.art !== 'fremd')
|
|
908
|
+
throw new FremderEintragUnbekannt(args.ziel, args.name);
|
|
909
|
+
await this.p.ziele.eintragLoeschen(args.ziel, verzeichnis, args.name);
|
|
910
|
+
await this.eintragBeachten({ ziel: args.ziel, name: args.name });
|
|
384
911
|
}
|
|
385
912
|
/**
|
|
386
913
|
* Plugin an einem Ziel herstellen: mit Plugin-Fähigkeit über die CLI des Harness, sonst als
|
|
@@ -416,6 +943,8 @@ export class Anwendungsfaelle {
|
|
|
416
943
|
async pluginLegen(args) {
|
|
417
944
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
418
945
|
const fund = await this.pluginFinden(args.plugin, args.quelle);
|
|
946
|
+
// Vor jeder Wirkung: kein Plugin, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
947
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
419
948
|
const ergebnis = await this.pluginHerstellen(harness, args.ziel, fund);
|
|
420
949
|
if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
|
|
421
950
|
return ergebnis;
|
|
@@ -466,6 +995,8 @@ export class Anwendungsfaelle {
|
|
|
466
995
|
async pluginEntfernen(args) {
|
|
467
996
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
468
997
|
const fund = await this.pluginFinden(args.plugin);
|
|
998
|
+
// Vor jeder Wirkung: kein Entfernen, wenn danach das Schreiben in den Zeiger scheitern würde (E57).
|
|
999
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
469
1000
|
const schluessel = pluginSchluessel(fund.plugin);
|
|
470
1001
|
const entfernt = [];
|
|
471
1002
|
if (kannPlugins(harness)) {
|
|
@@ -567,7 +1098,7 @@ export class Anwendungsfaelle {
|
|
|
567
1098
|
return ergebnis;
|
|
568
1099
|
}
|
|
569
1100
|
ergebnis.zeigerPfad = zeigerPfad;
|
|
570
|
-
const harnesses = (await this.
|
|
1101
|
+
const harnesses = (await this.vorhandeneAktiveHarnesses()).filter((h) => h.orte().includes('arbeitsverzeichnis'));
|
|
571
1102
|
if (harnesses.length === 0) {
|
|
572
1103
|
ergebnis.hinweise.push('Kein Harness auf diesem Rechner, nichts zu tun (E20).');
|
|
573
1104
|
return ergebnis;
|
|
@@ -660,11 +1191,28 @@ export class Anwendungsfaelle {
|
|
|
660
1191
|
* Aktualisieren (E14, E15): jeden Checkout unter der Wurzel auf den Stand seines Refs bringen.
|
|
661
1192
|
* Lokale Quellen haben keinen Checkout und bleiben unberührt (B11). Links zeigen auf die
|
|
662
1193
|
* Worktree-Pfade, die dabei stehen bleiben, also müssen sie nicht erneuert werden.
|
|
1194
|
+
*
|
|
1195
|
+
* Checkouts, deren Quelle der Zustand nicht führt, bleiben ganz außen vor (E54): sie werden weder
|
|
1196
|
+
* geholt noch als verwaist zum Löschen angeboten. Eine vergessene Quelle ist vergessen, ihre
|
|
1197
|
+
* Checkouts und Links liegen unangetastet, bis die Quelle wieder hinzukommt.
|
|
663
1198
|
*/
|
|
664
|
-
async aktualisieren() {
|
|
1199
|
+
async aktualisieren(args) {
|
|
1200
|
+
const rueckfragen = args?.rueckfragen ?? true;
|
|
1201
|
+
const bekannt = new Map((await this.p.zustand.lesen()).quellen.map((q) => [quelleSchluessel(q), q]));
|
|
665
1202
|
const checkouts = [];
|
|
666
|
-
for (const checkout of await this.p.quellen.checkouts())
|
|
667
|
-
|
|
1203
|
+
for (const checkout of await this.p.quellen.checkouts()) {
|
|
1204
|
+
const gefuehrt = bekannt.get(quelleSchluessel(checkout.quelle));
|
|
1205
|
+
if (gefuehrt === undefined)
|
|
1206
|
+
continue;
|
|
1207
|
+
const aktualisierung = await this.p.quellen.aktualisieren(checkout);
|
|
1208
|
+
// Der Quellen-Port baut Checkouts von der Platte (remote.origin.url), ohne den Zustand zu
|
|
1209
|
+
// befragen: der Anzeigename fehlt dort. Für den Bericht kommt er aus der geführten Quelle
|
|
1210
|
+
// (E59), sonst zeigte eine umbenannte Quelle hier wieder ihren abgeleiteten Namen.
|
|
1211
|
+
checkouts.push({
|
|
1212
|
+
checkout: { ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) },
|
|
1213
|
+
aktualisierung,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
668
1216
|
// Nach den Checkouts die Plugins nachziehen (E14).
|
|
669
1217
|
const pluginHarnesses = [];
|
|
670
1218
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
@@ -673,7 +1221,7 @@ export class Anwendungsfaelle {
|
|
|
673
1221
|
await harness.plugins.aktualisieren();
|
|
674
1222
|
pluginHarnesses.push(harness.name);
|
|
675
1223
|
}
|
|
676
|
-
return { checkouts, pluginHarnesses, aufraeumen: await this.verwaisteAufraeumen(checkouts) };
|
|
1224
|
+
return { checkouts, pluginHarnesses, aufraeumen: await this.verwaisteAufraeumen(checkouts, rueckfragen) };
|
|
677
1225
|
}
|
|
678
1226
|
/**
|
|
679
1227
|
* Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
|
|
@@ -697,8 +1245,11 @@ export class Anwendungsfaelle {
|
|
|
697
1245
|
* mit lokaler Änderung bleibt liegen und wird nur gemeldet (E15), er kommt gar nicht erst in die
|
|
698
1246
|
* Frage. Gefragt wird einmal für alle Kandidaten (E28); wo niemand antwortet, gilt Nein und es
|
|
699
1247
|
* wird nichts gelöscht.
|
|
1248
|
+
*
|
|
1249
|
+
* Bei `rueckfragen: false` (E44, automatischer GUI-Lauf) werden Verwaiste wie sonst ermittelt und
|
|
1250
|
+
* gemeldet, aber weder gefragt noch gelöscht.
|
|
700
1251
|
*/
|
|
701
|
-
async verwaisteAufraeumen(checkouts) {
|
|
1252
|
+
async verwaisteAufraeumen(checkouts, rueckfragen) {
|
|
702
1253
|
const belegt = await this.belegteCheckoutPfade();
|
|
703
1254
|
const verwaiste = [];
|
|
704
1255
|
for (const { checkout, aktualisierung } of checkouts) {
|
|
@@ -708,6 +1259,8 @@ export class Anwendungsfaelle {
|
|
|
708
1259
|
const lokaleAenderung = aktualisierung.ergebnis === 'uebersprungen';
|
|
709
1260
|
verwaiste.push({ checkout, lokaleAenderung, geloescht: false });
|
|
710
1261
|
}
|
|
1262
|
+
if (!rueckfragen)
|
|
1263
|
+
return { verwaiste, gefragt: false };
|
|
711
1264
|
const kandidaten = verwaiste.filter((v) => !v.lokaleAenderung);
|
|
712
1265
|
if (kandidaten.length === 0)
|
|
713
1266
|
return { verwaiste, gefragt: false };
|
|
@@ -720,14 +1273,124 @@ export class Anwendungsfaelle {
|
|
|
720
1273
|
return { verwaiste, gefragt: true };
|
|
721
1274
|
}
|
|
722
1275
|
/**
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
1276
|
+
* Erkennungskette eines fremden Eintrags (E31, E48), rein lesend, ohne etwas zu verändern. Sie
|
|
1277
|
+
* bedient Prognose (`istZustand`) und Übernahme gleichermaßen:
|
|
1278
|
+
* - Ein Symlink führt in eine Arbeitskopie (B12): Quelle und Ref aus `remote.origin.url` und dem
|
|
1279
|
+
* ausgecheckten Branch, der Skill über den relativen Pfad in der Quelle.
|
|
1280
|
+
* - Sonst gilt ein Ordner mit eigenem `.git` an Ort und Stelle als Arbeitskopie (E48 a): erkannt
|
|
1281
|
+
* nur, wenn sie sauber ist und ihr HEAD im lokalen Stand des Remote-Branchs enthalten ist,
|
|
1282
|
+
* sonst „lokale-aenderungen". Erkannt daran, dass `arbeitskopie` auf den Eintrag selbst
|
|
1283
|
+
* angewandt dessen eigene Wurzel liefert (`relativerPfad === ''`).
|
|
1284
|
+
* - Sonst greift bei einem agents-Ziel das Lockfile der skills-CLI (E48 b, HARNESS-FAKTEN):
|
|
1285
|
+
* `sourceUrl` und `ref` liefern die Quelle, ersetzt wird nur, wenn der Ordnerinhalt dem
|
|
1286
|
+
* festgehaltenen `skillFolderHash` entspricht. Das Hash-Verfahren der skills-CLI ist nur für
|
|
1287
|
+
* Nicht-GitHub-Quellen als SHA-256 über Dateien reproduzierbar (`computeSkillFolderHash`);
|
|
1288
|
+
* für GitHub-Quellen benutzt sie eine Baum-SHA aus der GitHub-API (40 Zeichen statt 64), die
|
|
1289
|
+
* sich ohne Netzzugriff nicht nachbilden lässt. Ein `skillFolderHash` mit 40 Zeichen ergibt
|
|
1290
|
+
* deshalb ehrlich „nicht-pruefbar" statt fälschlich „lokale-aenderungen" (E50); nur ein
|
|
1291
|
+
* nachrechenbarer (64-stelliger) Hash, der abweicht, ergibt „lokale-aenderungen".
|
|
1292
|
+
* - Sonst „kein-link": nichts davon trifft zu.
|
|
1293
|
+
* Ein kaputter Teil (unlesbares Lockfile, Git-Fehler) bricht die Erhebung nie ab, er ergibt einen
|
|
1294
|
+
* Grund. `bekannt` normalisiert die erkannte Quelle auf die schon geführte Schreibweise (E36),
|
|
1295
|
+
* ohne sie selbst zu verändern.
|
|
727
1296
|
*
|
|
728
|
-
* `
|
|
729
|
-
*
|
|
730
|
-
*
|
|
1297
|
+
* `nurLesend` trennt Prognose von Übernahme an genau einer Stelle: der Skill-Katalog einer
|
|
1298
|
+
* Quelle, die hmgr noch nicht führt, bleibt ungefragt. `skills()` legte sonst deren Basis-Clone
|
|
1299
|
+
* an, und die Erhebung würde für jeden fremden Symlink ein fremdes Repo aus dem Netz holen und
|
|
1300
|
+
* auf die Platte schreiben. Bekannte Quellen haben ihren Clone schon, sie werden wie bisher
|
|
1301
|
+
* gefragt. Die Prognose nennt dann Quelle und Ref ohne Skill (E48), „skill-unbekannt" fällt in
|
|
1302
|
+
* diesem Fall erst in der Vorschau der Übernahme auf.
|
|
1303
|
+
*/
|
|
1304
|
+
async erkennenFremd(harness, verzeichnis, eintrag, bekannt, nurLesend) {
|
|
1305
|
+
const unbekannt = (grund) => ({ art: 'unbekannt', grund });
|
|
1306
|
+
const normalisiert = (kandidat) => bekannt.get(quelleSchluessel(kandidat)) ?? kandidat;
|
|
1307
|
+
/** Skill aus dem Katalog: `null` heißt „nicht darin", `undefined` heißt „nicht gefragt". */
|
|
1308
|
+
const ausKatalog = async (quelle, ref, pfad) => {
|
|
1309
|
+
if (nurLesend && !bekannt.has(quelleSchluessel(quelle)))
|
|
1310
|
+
return undefined;
|
|
1311
|
+
const skills = await this.p.quellen.skills(quelle, ref).catch(() => []);
|
|
1312
|
+
return skills.find((s) => s.pfad === pfad) ?? null;
|
|
1313
|
+
};
|
|
1314
|
+
if (eintrag.linkZiel !== undefined) {
|
|
1315
|
+
const kopie = await this.p.quellen.arbeitskopie(eintrag.linkZiel).catch(() => null);
|
|
1316
|
+
if (kopie === null)
|
|
1317
|
+
return unbekannt('keine-arbeitskopie');
|
|
1318
|
+
if (kopie.url === null)
|
|
1319
|
+
return unbekannt('keine-url');
|
|
1320
|
+
if (kopie.ref === null)
|
|
1321
|
+
return unbekannt('kein-branch');
|
|
1322
|
+
const quelle = normalisiert({ art: 'git', url: kopie.url });
|
|
1323
|
+
const skill = await ausKatalog(quelle, kopie.ref, kopie.relativerPfad);
|
|
1324
|
+
if (skill === null)
|
|
1325
|
+
return unbekannt('skill-unbekannt');
|
|
1326
|
+
return { art: 'erkannt', quelle, ref: kopie.ref, ...(skill === undefined ? {} : { skill }) };
|
|
1327
|
+
}
|
|
1328
|
+
const entryPfad = path.join(verzeichnis, eintrag.name);
|
|
1329
|
+
// (a) Ordner mit eigenem .git an Ort und Stelle (E48).
|
|
1330
|
+
const eigeneKopie = await this.p.quellen.arbeitskopie(entryPfad).catch(() => null);
|
|
1331
|
+
if (eigeneKopie !== null && eigeneKopie.relativerPfad === '') {
|
|
1332
|
+
if (eigeneKopie.url === null)
|
|
1333
|
+
return unbekannt('keine-url');
|
|
1334
|
+
if (eigeneKopie.ref === null)
|
|
1335
|
+
return unbekannt('kein-branch');
|
|
1336
|
+
const quelleRoh = { art: 'git', url: eigeneKopie.url };
|
|
1337
|
+
const sauber = !(await this.p.quellen
|
|
1338
|
+
.lokaleAenderungen({ quelle: quelleRoh, ref: eigeneKopie.ref, pfad: entryPfad })
|
|
1339
|
+
.catch(() => true));
|
|
1340
|
+
const enthalten = sauber && (await this.p.quellen.imRemoteEnthalten(entryPfad, eigeneKopie.ref).catch(() => false));
|
|
1341
|
+
if (!enthalten)
|
|
1342
|
+
return unbekannt('lokale-aenderungen');
|
|
1343
|
+
const quelle = normalisiert(quelleRoh);
|
|
1344
|
+
// Der Katalog (skills()) kennt einen Wurzel-Skill zwar seit dem E11-Nachtrag, aber er gilt
|
|
1345
|
+
// für die geführte Quelle am gefragten Ref. Hier liegt eine fremde Arbeitskopie an Ort und
|
|
1346
|
+
// Stelle: ihr Wurzel-Skill wird direkt von der Platte gelesen, ohne Clone (E48 a).
|
|
1347
|
+
const skill = await this.p.quellen.wurzelSkill(entryPfad).catch(() => null);
|
|
1348
|
+
if (skill === null)
|
|
1349
|
+
return unbekannt('skill-unbekannt');
|
|
1350
|
+
return { art: 'erkannt', quelle, ref: eigeneKopie.ref, skill };
|
|
1351
|
+
}
|
|
1352
|
+
// (b) Lockfile der skills-CLI, nur für agents-Ziele (E48 b).
|
|
1353
|
+
if (harness.name === 'agents') {
|
|
1354
|
+
const skillsHeim = harness.zielVerzeichnis({ art: 'home' });
|
|
1355
|
+
const lockEintrag = await this.p.ziele
|
|
1356
|
+
.skillLockEintrag(skillsHeim, eintrag.name)
|
|
1357
|
+
.catch(() => undefined);
|
|
1358
|
+
if (lockEintrag !== undefined) {
|
|
1359
|
+
if (lockEintrag.ref === undefined)
|
|
1360
|
+
return unbekannt('kein-branch');
|
|
1361
|
+
if (lockEintrag.skillPath === undefined)
|
|
1362
|
+
return unbekannt('skill-unbekannt');
|
|
1363
|
+
// Eine GitHub-Baum-SHA (40 Hex-Zeichen) statt eines SHA-256 (64) lässt sich lokal nicht
|
|
1364
|
+
// nachrechnen (E50): ehrlich „nicht prüfbar" statt fälschlich „lokale Änderungen".
|
|
1365
|
+
if (lockEintrag.skillFolderHash.length === 40)
|
|
1366
|
+
return unbekannt('nicht-pruefbar');
|
|
1367
|
+
const echterHash = await this.p.ziele.ordnerHash(verzeichnis, eintrag.name).catch(() => undefined);
|
|
1368
|
+
if (echterHash === undefined || echterHash.toLowerCase() !== lockEintrag.skillFolderHash.toLowerCase())
|
|
1369
|
+
return unbekannt('lokale-aenderungen');
|
|
1370
|
+
const quelle = normalisiert({ art: 'git', url: lockEintrag.sourceUrl });
|
|
1371
|
+
const skill = await ausKatalog(quelle, lockEintrag.ref, skillPfadAusLock(lockEintrag.skillPath));
|
|
1372
|
+
if (skill === null)
|
|
1373
|
+
return unbekannt('skill-unbekannt');
|
|
1374
|
+
return { art: 'erkannt', quelle, ref: lockEintrag.ref, ...(skill === undefined ? {} : { skill }) };
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return unbekannt('kein-link');
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Übernahme (B12, E31): fremde Einträge eines Ziels durch eigene Links auf einen hmgr-Checkout
|
|
1381
|
+
* derselben Quelle am selben Ref ersetzen. Eine Arbeitskopie, in die ein Symlink führt, bleibt
|
|
1382
|
+
* unberührt und wird nicht als Quelle registriert, das ist B11. Ein Ordner mit eigenem `.git`
|
|
1383
|
+
* oder ein Lockfile-Treffer wird dagegen selbst gelöscht und durch den Link ersetzt (E48 a/b). Ein
|
|
1384
|
+
* Eintrag, der nicht auflösbar ist, bleibt liegen und wird gemeldet, damit ein Lauf über viele
|
|
1385
|
+
* Einträge nicht an einem einzelnen abbricht.
|
|
1386
|
+
*
|
|
1387
|
+
* `probelauf` schreibt weder Zustand noch Checkout noch Link. Die Erkennungskette wird trotzdem
|
|
1388
|
+
* gelesen, sonst wären die Gründe nicht zu haben. Der Git-Adapter legt dabei den Basis-Clone der
|
|
1389
|
+
* Quelle an, das Ziel und der Zustand bleiben unangetastet.
|
|
1390
|
+
*
|
|
1391
|
+
* Mit `name` (E46) betrachtet der Lauf nur diesen einen fremden Eintrag, ohne `name` bleibt es
|
|
1392
|
+
* bei allen fremden Einträgen des Ziels (CLI unverändert). Ein genannter Name, der am Ziel nicht
|
|
1393
|
+
* als fremder Eintrag liegt, ist ein Fehler, gilt für Probelauf wie echten Lauf gleichermaßen.
|
|
731
1394
|
*/
|
|
732
1395
|
async uebernehmen(args) {
|
|
733
1396
|
const harness = await this.harnessGeprueft(args.ziel.harness);
|
|
@@ -740,60 +1403,93 @@ export class Anwendungsfaelle {
|
|
|
740
1403
|
for (const q of (await this.p.zustand.lesen()).quellen)
|
|
741
1404
|
bekannt.set(quelleSchluessel(q), q);
|
|
742
1405
|
const eintraege = [];
|
|
743
|
-
|
|
1406
|
+
const alleEintraege = await this.p.ziele.eintraege(args.ziel, verzeichnis);
|
|
1407
|
+
if (args.name !== undefined) {
|
|
1408
|
+
const gefunden = alleEintraege.find((e) => (e.art === 'link' ? e.link.name : e.name) === args.name);
|
|
1409
|
+
if (gefunden === undefined || gefunden.art !== 'fremd')
|
|
1410
|
+
throw new FremderEintragUnbekannt(args.ziel, args.name);
|
|
1411
|
+
}
|
|
1412
|
+
for (const eintrag of alleEintraege) {
|
|
744
1413
|
// Eigene Links sind schon fertig, sie kommen nicht in die Meldung.
|
|
745
1414
|
if (eintrag.art !== 'fremd')
|
|
746
1415
|
continue;
|
|
747
1416
|
if (args.name !== undefined && eintrag.name !== args.name)
|
|
748
1417
|
continue;
|
|
749
1418
|
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');
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
if (kopie.url === null) {
|
|
761
|
-
liegenLassen('keine-url');
|
|
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');
|
|
1419
|
+
const prognose = await this.erkennenFremd(harness, verzeichnis, eintrag, bekannt, false);
|
|
1420
|
+
if (prognose.art === 'unbekannt') {
|
|
1421
|
+
eintraege.push({ ergebnis: 'uebersprungen', name, grund: prognose.grund });
|
|
767
1422
|
continue;
|
|
768
1423
|
}
|
|
769
|
-
|
|
770
|
-
|
|
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);
|
|
1424
|
+
const { quelle, ref, skill } = prognose;
|
|
1425
|
+
// Ohne `nurLesend` fragt die Kette den Katalog immer, ein fehlender Skill kommt hier nie an.
|
|
775
1426
|
if (skill === undefined) {
|
|
776
|
-
|
|
1427
|
+
eintraege.push({ ergebnis: 'uebersprungen', name, grund: 'skill-unbekannt' });
|
|
777
1428
|
continue;
|
|
778
1429
|
}
|
|
779
|
-
const quelleNeu =
|
|
1430
|
+
const quelleNeu = !bekannt.has(quelleSchluessel(quelle));
|
|
780
1431
|
bekannt.set(quelleSchluessel(quelle), quelle);
|
|
1432
|
+
const ordnerErsetzt = eintrag.linkZiel === undefined;
|
|
781
1433
|
if (!probelauf) {
|
|
1434
|
+
// Vor jeder Wirkung (E57): die Übernahme schreibt ohne `lokal` denselben Zeiger wie
|
|
1435
|
+
// `link`. Die Sperre muss vor `quelleHinzufuegen` greifen, sonst bliebe die Quelle im
|
|
1436
|
+
// Zustand stehen, während der Link nie entsteht. Ein früherer Durchlauf kann nichts
|
|
1437
|
+
// geschrieben haben: er wäre an derselben Stelle abgebrochen.
|
|
1438
|
+
await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
|
|
1439
|
+
// Der abgeleitete Anzeigename kann schon vergeben sein (E59). Die Übernahme hat keinen Weg,
|
|
1440
|
+
// den Nutzer nach einem anderen zu fragen, darum weicht sie auf einen freien aus statt an
|
|
1441
|
+
// QuelleNameBelegt zu scheitern; umbenennen lässt er sich hinterher (source rename).
|
|
782
1442
|
if (quelleNeu)
|
|
783
|
-
await this.quelleHinzufuegen(quelle);
|
|
1443
|
+
await this.quelleHinzufuegen(quelle, quelleNameFrei(quelleNameAbleiten(quelle), (await this.p.zustand.lesen()).quellen));
|
|
784
1444
|
await this.linkAusFund(harness, args.ziel, { quelle, ref, skill }, true, args.lokal);
|
|
785
1445
|
}
|
|
786
|
-
|
|
1446
|
+
// Nur in der Vorschau (E50): welche gitignorierten Dateien beim Ersetzen verloren gingen.
|
|
1447
|
+
// Blockiert nichts, ist reine Sichtbarkeit vor dem echten Lauf.
|
|
1448
|
+
let gitignoriert;
|
|
1449
|
+
if (probelauf && ordnerErsetzt) {
|
|
1450
|
+
const liste = await this.p.quellen
|
|
1451
|
+
.gitignorierteDateien(path.join(verzeichnis, name))
|
|
1452
|
+
.catch(() => []);
|
|
1453
|
+
if (liste.length > 0)
|
|
1454
|
+
gitignoriert = liste;
|
|
1455
|
+
}
|
|
1456
|
+
eintraege.push({
|
|
1457
|
+
ergebnis: 'uebernommen',
|
|
1458
|
+
name,
|
|
1459
|
+
quelle,
|
|
1460
|
+
ref,
|
|
1461
|
+
skill,
|
|
1462
|
+
quelleNeu,
|
|
1463
|
+
ordnerErsetzt,
|
|
1464
|
+
...(gitignoriert === undefined ? {} : { gitignoriert }),
|
|
1465
|
+
});
|
|
787
1466
|
}
|
|
1467
|
+
// Ausdrücklich lokal an einem gebundenen Arbeitsverzeichnis (E57, F32): der Zeiger bleibt
|
|
1468
|
+
// unangetastet, ein Hinweis sagt das. Nur wenn wirklich etwas übernommen wurde und nur bei
|
|
1469
|
+
// bestehender Bindung, sonst wäre es Rauschen.
|
|
1470
|
+
if (!probelauf &&
|
|
1471
|
+
args.lokal === true &&
|
|
1472
|
+
args.ziel.ort.art === 'arbeitsverzeichnis' &&
|
|
1473
|
+
eintraege.some((e) => e.ergebnis === 'uebernommen') &&
|
|
1474
|
+
(await this.bindungFinden(args.ziel)) !== undefined)
|
|
1475
|
+
return {
|
|
1476
|
+
ziel: args.ziel,
|
|
1477
|
+
probelauf,
|
|
1478
|
+
eintraege,
|
|
1479
|
+
hinweis: 'Die übernommenen Links liegen nur lokal, der Zeiger bleibt unangetastet (F32).',
|
|
1480
|
+
};
|
|
788
1481
|
return { ziel: args.ziel, probelauf, eintraege };
|
|
789
1482
|
}
|
|
790
|
-
/**
|
|
791
|
-
async
|
|
792
|
-
const ergebnis = new Map();
|
|
1483
|
+
/** Zeiger, an den ein Ziel gebunden ist. Ohne Bindung oder mit unlesbarer Datei: undefined. */
|
|
1484
|
+
async zeigerFuerZiel(ziel) {
|
|
793
1485
|
const zeigerPfad = await this.bindungFinden(ziel);
|
|
794
1486
|
if (zeigerPfad === undefined)
|
|
795
|
-
return
|
|
796
|
-
|
|
1487
|
+
return undefined;
|
|
1488
|
+
return await this.p.zeiger.lesen(zeigerPfad).catch(() => undefined);
|
|
1489
|
+
}
|
|
1490
|
+
/** Versionseinschränkungen aus dem Zeiger eines Ziels (E33). Ohne Zeiger leer. */
|
|
1491
|
+
einschraenkungen(zeiger) {
|
|
1492
|
+
const ergebnis = new Map();
|
|
797
1493
|
if (zeiger === undefined)
|
|
798
1494
|
return ergebnis;
|
|
799
1495
|
for (const [name, einschraenkung] of zeiger.skills)
|
|
@@ -801,9 +1497,80 @@ export class Anwendungsfaelle {
|
|
|
801
1497
|
ergebnis.set(name, einschraenkung);
|
|
802
1498
|
return ergebnis;
|
|
803
1499
|
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Plugins, die für ein Ziel als Grund in Frage kommen: bei einem Arbeitsverzeichnis die des
|
|
1502
|
+
* gebundenen Zeigers, bei Home und Profil die aller Marketplaces der bekannten Quellen.
|
|
1503
|
+
*/
|
|
1504
|
+
async pluginKandidaten(ziel, zeiger, marktplatzFunde) {
|
|
1505
|
+
const alle = await marktplatzFunde();
|
|
1506
|
+
if (ziel.ort.art !== 'arbeitsverzeichnis')
|
|
1507
|
+
return alle;
|
|
1508
|
+
if (zeiger === undefined)
|
|
1509
|
+
return [];
|
|
1510
|
+
const funde = [];
|
|
1511
|
+
for (const eintrag of zeiger.plugins) {
|
|
1512
|
+
const zerlegt = pluginSchluesselZerlegen(eintrag);
|
|
1513
|
+
for (const fund of alle)
|
|
1514
|
+
if (fund.plugin.name === zerlegt.name &&
|
|
1515
|
+
(zerlegt.marketplace === undefined || fund.plugin.marketplace === zerlegt.marketplace))
|
|
1516
|
+
funde.push(fund);
|
|
1517
|
+
}
|
|
1518
|
+
return funde;
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Grund je eigenem Link (E37). Priorität: Plugin vor Zeiger vor manuell. Die Plugin-Zugehörigkeit
|
|
1522
|
+
* steht vorn, weil sie die Einschränkung trägt, dass der Link nicht einzeln entfernt werden darf,
|
|
1523
|
+
* ohne das Plugin zu verletzen. Damit gilt auch ein von Hand gelegter Link, der eine flache
|
|
1524
|
+
* Skill-Liste komplettiert, als Plugin-Link. Diese Unschärfe ist bekannt und akzeptiert.
|
|
1525
|
+
* Bei einem Harness mit Plugin-Fähigkeit sind Plugin-Skills keine hmgr-Links (E16), dort entsteht
|
|
1526
|
+
* der Plugin-Grund nicht. Ist eine Quelle nicht erreichbar, fällt ihr Plugin als Kandidat aus.
|
|
1527
|
+
*/
|
|
1528
|
+
async gruende(harness, ziel, eintraege, zeiger, marktplatzFunde) {
|
|
1529
|
+
const gruende = new Map();
|
|
1530
|
+
const eigene = eintraege.filter((e) => e.art !== 'fremd').map((e) => (e.art === 'link' ? e.link.name : e.name));
|
|
1531
|
+
if (eigene.length === 0)
|
|
1532
|
+
return gruende;
|
|
1533
|
+
const eigeneNamen = new Set(eigene);
|
|
1534
|
+
if (!kannPlugins(harness))
|
|
1535
|
+
for (const fund of await this.pluginKandidaten(ziel, zeiger, marktplatzFunde)) {
|
|
1536
|
+
if (!(await this.pluginLiegt(harness, ziel, fund, eintraege).catch(() => false)))
|
|
1537
|
+
continue;
|
|
1538
|
+
const skills = await this.flacheSkillListe(fund.plugin.name, fund.quelle).catch(() => []);
|
|
1539
|
+
const schluessel = pluginSchluessel(fund.plugin);
|
|
1540
|
+
for (const skill of skills)
|
|
1541
|
+
if (eigeneNamen.has(skill.name) && !gruende.has(skill.name))
|
|
1542
|
+
gruende.set(skill.name, { art: 'plugin', plugin: schluessel });
|
|
1543
|
+
}
|
|
1544
|
+
for (const name of eigene)
|
|
1545
|
+
if (!gruende.has(name))
|
|
1546
|
+
gruende.set(name, zeiger?.skills.has(name) === true ? { art: 'zeiger' } : { art: 'manuell' });
|
|
1547
|
+
return gruende;
|
|
1548
|
+
}
|
|
804
1549
|
/** Ist-Zustand: alle Ziele der vorhandenen Harnesses, aus dem Dateisystem gelesen. */
|
|
805
1550
|
async istZustand() {
|
|
806
1551
|
const zustand = await this.p.zustand.lesen();
|
|
1552
|
+
// Je Identität die Quelle, unter deren Schreibweise sie geführt wird (E36, E48), für die
|
|
1553
|
+
// Übernahme-Prognose. Bleibt über den ganzen Lauf gleich, die Prognose verändert nichts.
|
|
1554
|
+
const bekannt = new Map();
|
|
1555
|
+
for (const q of zustand.quellen)
|
|
1556
|
+
bekannt.set(quelleSchluessel(q), q);
|
|
1557
|
+
// Marketplaces der bekannten Quellen, höchstens einmal je Lauf gelesen. Eine unerreichbare
|
|
1558
|
+
// Quelle fällt aus, statt die Erhebung abzubrechen.
|
|
1559
|
+
let gelesene;
|
|
1560
|
+
const marktplatzFunde = async () => {
|
|
1561
|
+
if (gelesene !== undefined)
|
|
1562
|
+
return gelesene;
|
|
1563
|
+
const funde = [];
|
|
1564
|
+
for (const quelle of zustand.quellen) {
|
|
1565
|
+
const marktplatz = await this.marketplaceLesen(quelle).catch(() => null);
|
|
1566
|
+
if (marktplatz === null)
|
|
1567
|
+
continue;
|
|
1568
|
+
for (const plugin of marktplatz.plugins)
|
|
1569
|
+
funde.push({ quelle, ref: marktplatz.ref, plugin });
|
|
1570
|
+
}
|
|
1571
|
+
gelesene = funde;
|
|
1572
|
+
return funde;
|
|
1573
|
+
};
|
|
807
1574
|
const ergebnis = [];
|
|
808
1575
|
for (const harness of await this.vorhandeneHarnesses()) {
|
|
809
1576
|
const ziele = [];
|
|
@@ -821,12 +1588,39 @@ export class Anwendungsfaelle {
|
|
|
821
1588
|
const plugins = kannPlugins(harness)
|
|
822
1589
|
? await harness.plugins.installierte(ziel).catch(() => [])
|
|
823
1590
|
: undefined;
|
|
1591
|
+
const zeiger = await this.zeigerFuerZiel(ziel);
|
|
1592
|
+
const istRegistriert = kannIstRegistriert(harness) ? await harness.istRegistriert(ziel.ort) : false;
|
|
1593
|
+
// Fähigkeit am Typwächter, nicht am Namen (E16), und nur an Orten, die sie kennt.
|
|
1594
|
+
let skillOrdnerFaehig = false;
|
|
1595
|
+
let verwalteterSkillOrdner;
|
|
1596
|
+
if (kannSkillOrdner(harness) && harness.skillOrdnerOrte().includes(ziel.ort.art)) {
|
|
1597
|
+
skillOrdnerFaehig = true;
|
|
1598
|
+
verwalteterSkillOrdner = await harness.verwalteterSkillOrdner(ziel.ort);
|
|
1599
|
+
}
|
|
1600
|
+
// Ignoriert (E43): nur Namen fremder Einträge, die tatsächlich noch auf der Platte liegen.
|
|
1601
|
+
// Die Ignorier-Liste selbst wird dabei nicht bereinigt.
|
|
1602
|
+
const fremdeNamen = new Set(eintraege.filter((ei) => ei.art === 'fremd').map((ei) => ei.name));
|
|
1603
|
+
const zielSchl = zielSchluessel(ziel);
|
|
1604
|
+
const ignoriert = zustand.ignorierteEintraege
|
|
1605
|
+
.filter((i) => zielSchluessel({ harness: i.harness, ort: i.ort }) === zielSchl && fremdeNamen.has(i.name))
|
|
1606
|
+
.map((i) => i.name);
|
|
1607
|
+
// Übernahme-Prognose je fremdem Eintrag (E48), dieselbe Kette wie `uebernehmen`, nur lesend.
|
|
1608
|
+
const uebernahme = new Map();
|
|
1609
|
+
for (const ei of eintraege)
|
|
1610
|
+
if (ei.art === 'fremd')
|
|
1611
|
+
uebernahme.set(ei.name, await this.erkennenFremd(harness, verzeichnis, ei, bekannt, true));
|
|
824
1612
|
ergebnis.push({
|
|
825
1613
|
ziel,
|
|
826
1614
|
verzeichnis,
|
|
827
1615
|
eintraege,
|
|
828
|
-
einschraenkungen:
|
|
1616
|
+
einschraenkungen: this.einschraenkungen(zeiger),
|
|
1617
|
+
gruende: await this.gruende(harness, ziel, eintraege, zeiger, marktplatzFunde),
|
|
1618
|
+
ignoriert,
|
|
1619
|
+
uebernahme,
|
|
1620
|
+
istRegistriert,
|
|
1621
|
+
kannSkillOrdner: skillOrdnerFaehig,
|
|
829
1622
|
...(plugins === undefined ? {} : { plugins }),
|
|
1623
|
+
...(verwalteterSkillOrdner === undefined ? {} : { verwalteterSkillOrdner }),
|
|
830
1624
|
});
|
|
831
1625
|
}
|
|
832
1626
|
}
|