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
package/dist/cli/main.js
CHANGED
|
@@ -5,20 +5,27 @@ import { realpathSync } from 'node:fs';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
|
|
8
|
-
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, pluginSchluessel, } from "../kern/modell.js";
|
|
8
|
+
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
|
|
9
9
|
import { Agents } from "../adapter/agents.js";
|
|
10
10
|
import { ClaudeCode } from "../adapter/claude-code.js";
|
|
11
11
|
import { Hermes } from "../adapter/hermes.js";
|
|
12
12
|
import { ComposerZeiger } from "../adapter/composer.js";
|
|
13
13
|
import { DateisystemZiele } from "../adapter/dateisystem.js";
|
|
14
14
|
import { GitQuellen } from "../adapter/git.js";
|
|
15
|
+
import { DateiMcpKatalog } from "../adapter/mcp-katalog.js";
|
|
15
16
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
16
17
|
import { TerminalRueckfrage } from "../adapter/terminal.js";
|
|
18
|
+
import { ProzessUmgebung } from "../adapter/umgebung.js";
|
|
17
19
|
import { ZustandJson } from "../adapter/zustand-json.js";
|
|
18
20
|
import { HILFE, hilfeText } from "./hilfe.js";
|
|
19
21
|
export function umgebungLesen(env = process.env) {
|
|
20
22
|
const home = env.HOME ?? homedir();
|
|
21
|
-
|
|
23
|
+
const xdgStateHome = env.XDG_STATE_HOME;
|
|
24
|
+
return {
|
|
25
|
+
home,
|
|
26
|
+
hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr'),
|
|
27
|
+
...(xdgStateHome !== undefined && xdgStateHome.length > 0 ? { xdgStateHome } : {}),
|
|
28
|
+
};
|
|
22
29
|
}
|
|
23
30
|
export function bauen(umgebung, optionen = {}) {
|
|
24
31
|
const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'));
|
|
@@ -29,10 +36,12 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
29
36
|
const ports = {
|
|
30
37
|
harnesses: [claude, new Hermes(umgebung.home), new Agents(umgebung.home)],
|
|
31
38
|
quellen: new GitQuellen(wurzel),
|
|
32
|
-
ziele: new DateisystemZiele(wurzel, zustand),
|
|
39
|
+
ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome),
|
|
33
40
|
zeiger: new ComposerZeiger(),
|
|
34
41
|
zustand,
|
|
35
42
|
rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
|
|
43
|
+
mcpKatalog: new DateiMcpKatalog(path.join(umgebung.hmgrHome, 'mcp.yaml')),
|
|
44
|
+
umgebung: optionen.umgebung ?? new ProzessUmgebung(),
|
|
36
45
|
};
|
|
37
46
|
return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
|
|
38
47
|
}
|
|
@@ -41,7 +50,7 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
41
50
|
* steht nirgends, weil der Hilfe-Dispatch vor jedem schalterLesen läuft.
|
|
42
51
|
*/
|
|
43
52
|
const SCHALTER = {
|
|
44
|
-
source: { werte: [], flaggen: [] },
|
|
53
|
+
source: { werte: ['name'], flaggen: [] },
|
|
45
54
|
dir: { werte: ['composer'], flaggen: [] },
|
|
46
55
|
link: {
|
|
47
56
|
werte: ['harness', 'ort', 'composer', 'quelle', 'version'],
|
|
@@ -50,9 +59,11 @@ const SCHALTER = {
|
|
|
50
59
|
unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
|
|
51
60
|
adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
|
|
52
61
|
plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
|
|
62
|
+
mcp: { werte: ['harness', 'ort'], flaggen: [] },
|
|
53
63
|
sync: { werte: [], flaggen: [] },
|
|
54
64
|
update: { werte: [], flaggen: [] },
|
|
55
65
|
status: { werte: [], flaggen: [] },
|
|
66
|
+
harness: { werte: ['profil'], flaggen: [] },
|
|
56
67
|
};
|
|
57
68
|
/**
|
|
58
69
|
* Was ein einzelnes Unterkommando von seiner Familie liest, gleichlautend mit ihrer Hilfe. Ohne
|
|
@@ -60,7 +71,12 @@ const SCHALTER = {
|
|
|
60
71
|
* Schalter lesen.
|
|
61
72
|
*/
|
|
62
73
|
const UNTERKOMMANDOS = {
|
|
63
|
-
source: {
|
|
74
|
+
source: {
|
|
75
|
+
add: { woerter: 2, schalter: ['name'] },
|
|
76
|
+
remove: { woerter: 2, schalter: [] },
|
|
77
|
+
rename: { woerter: 3, schalter: [] },
|
|
78
|
+
list: { woerter: 1, schalter: [] },
|
|
79
|
+
},
|
|
64
80
|
dir: {
|
|
65
81
|
add: { woerter: 2, schalter: ['composer'] },
|
|
66
82
|
forget: { woerter: 2, schalter: [] },
|
|
@@ -71,6 +87,18 @@ const UNTERKOMMANDOS = {
|
|
|
71
87
|
remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
|
|
72
88
|
list: { woerter: 1, schalter: [] },
|
|
73
89
|
},
|
|
90
|
+
mcp: {
|
|
91
|
+
add: { woerter: 2, schalter: ['harness', 'ort'] },
|
|
92
|
+
remove: { woerter: 2, schalter: ['harness', 'ort'] },
|
|
93
|
+
// Übernehmen (E64): dieselben Ziel-Schalter wie add und remove.
|
|
94
|
+
migrate: { woerter: 2, schalter: ['harness', 'ort'] },
|
|
95
|
+
list: { woerter: 1, schalter: [] },
|
|
96
|
+
},
|
|
97
|
+
harness: {
|
|
98
|
+
list: { woerter: 1, schalter: [] },
|
|
99
|
+
enable: { woerter: 2, schalter: [] },
|
|
100
|
+
disable: { woerter: 2, schalter: [] },
|
|
101
|
+
},
|
|
74
102
|
};
|
|
75
103
|
function schalterLesen(args, kommando) {
|
|
76
104
|
const erlaubt = SCHALTER[kommando];
|
|
@@ -145,6 +173,8 @@ const UEBERNAHME_GRUND = {
|
|
|
145
173
|
'keine-url': 'Arbeitskopie ohne remote.origin.url',
|
|
146
174
|
'kein-branch': 'Arbeitskopie ohne Branch (detached HEAD)',
|
|
147
175
|
'skill-unbekannt': 'Pfad ist in der Quelle kein Skill',
|
|
176
|
+
'lokale-aenderungen': 'Ordner weicht von der Quelle ab',
|
|
177
|
+
'nicht-pruefbar': 'hmgr kann nicht prüfen, ob der Ordner der Quelle entspricht',
|
|
148
178
|
};
|
|
149
179
|
export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
150
180
|
const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
|
|
@@ -176,22 +206,24 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
176
206
|
if (kommando === 'source') {
|
|
177
207
|
const schalter = schalterLesen(argv.slice(1), 'source');
|
|
178
208
|
unterkommandoPruefen('source', schalter);
|
|
179
|
-
const rest = schalter
|
|
209
|
+
const { rest, werte } = schalter;
|
|
180
210
|
const unterkommando = rest[0];
|
|
181
211
|
if (unterkommando === 'add') {
|
|
182
212
|
const text = rest[1];
|
|
183
213
|
if (text === undefined)
|
|
184
214
|
throw new Error('source add braucht eine URL oder einen Pfad, etwa hmgr source add https://example.com/skills.git');
|
|
185
215
|
const genannt = quelleAusText(text);
|
|
186
|
-
const { quelle, skills, marketplace, bekannt } = await anwendungsfaelle.quelleHinzufuegen(genannt);
|
|
216
|
+
const { quelle, skills, marketplace, bekannt } = await anwendungsfaelle.quelleHinzufuegen(genannt, werte.get('name'));
|
|
187
217
|
// Dieselbe Quelle in anderer Schreibweise legt keine zweite an (E36).
|
|
188
218
|
if (bekannt)
|
|
189
219
|
console.log(quelleAlsText(quelle) === quelleAlsText(genannt)
|
|
190
220
|
? 'Quelle ist schon bekannt, keine zweite angelegt.'
|
|
191
221
|
: `Quelle ist schon bekannt, geführt als ${quelleAlsText(quelle)}. Keine zweite angelegt.`);
|
|
192
|
-
console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}), ${skills.length} Skills:`);
|
|
193
|
-
for (const skill of skills)
|
|
194
|
-
|
|
222
|
+
console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}) als „${quelleName(quelle)}", ${skills.length} ${skills.length === 1 ? 'Skill' : 'Skills'}:`);
|
|
223
|
+
for (const skill of skills) {
|
|
224
|
+
const beschreibung = skill.beschreibung === undefined ? '' : ` ${kappen(skill.beschreibung)}`;
|
|
225
|
+
console.log(` ${skill.name} ${skill.pfad}${beschreibung}`);
|
|
226
|
+
}
|
|
195
227
|
if (marketplace !== null) {
|
|
196
228
|
console.log(`Marketplace ${marketplace.name}, ${marketplace.plugins.length} Plugins:`);
|
|
197
229
|
for (const plugin of marketplace.plugins) {
|
|
@@ -201,15 +233,89 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
201
233
|
}
|
|
202
234
|
return 0;
|
|
203
235
|
}
|
|
236
|
+
if (unterkommando === 'remove') {
|
|
237
|
+
const text = rest[1];
|
|
238
|
+
if (text === undefined)
|
|
239
|
+
throw new Error('source remove braucht eine URL oder einen Pfad. hmgr source list zeigt die bekannten Quellen.');
|
|
240
|
+
const genannt = quelleAusText(text);
|
|
241
|
+
// Inventar (E58) vor dem Vergessen erheben (F14): danach tragen die Links ihre Quelle nicht
|
|
242
|
+
// mehr. Bei unbekannter Quelle entfällt der Ziel-Scan, den Fehler wirft quelleEntfernen.
|
|
243
|
+
const key = quelleSchluessel(genannt);
|
|
244
|
+
// Die Rückfrage kommt jetzt VOR dem Vergessen (F31a), nennen muss sie die Quelle trotzdem
|
|
245
|
+
// unter ihrer geführten Schreibweise (E36), nicht unter der getippten.
|
|
246
|
+
const gefuehrtVorher = (await anwendungsfaelle.quellen()).find((q) => quelleSchluessel(q) === key);
|
|
247
|
+
const inventar = gefuehrtVorher === undefined ? undefined : await anwendungsfaelle.quelleAufraeumInventar(genannt);
|
|
248
|
+
const meldeVergessen = (gefuehrt, zahl) => {
|
|
249
|
+
console.log(`Quelle ${quelleAlsText(gefuehrt)} vergessen, Checkouts und Links bleiben liegen.`);
|
|
250
|
+
if (zahl === 0)
|
|
251
|
+
console.log(' Kein Link betroffen.');
|
|
252
|
+
else {
|
|
253
|
+
console.log(` ${zahl === 1 ? '1 Link gilt' : `${zahl} Links gelten`} jetzt als „Link ohne bekannte Quelle", das Aktualisieren prüft ${zahl === 1 ? 'ihn' : 'sie'} nicht mehr.`);
|
|
254
|
+
console.log(` source add derselben Quelle macht ${zahl === 1 ? 'ihn wieder zu einem normalen Link' : 'sie wieder zu normalen Links'}.`);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
// Leeres Inventar (F31a): nichts zum Aufräumen, keine Rückfrage, sofort vergessen wie bisher.
|
|
258
|
+
if (gefuehrtVorher === undefined ||
|
|
259
|
+
inventar === undefined ||
|
|
260
|
+
(inventar.links.length === 0 && inventar.checkouts.length === 0)) {
|
|
261
|
+
const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
|
|
262
|
+
meldeVergessen(gefuehrt, inventar?.links.length ?? 0);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
// Aufräum-Rückfrage VOR dem Vergessen (F31a): ein Ja mit lokalen Änderungen darf weder löschen
|
|
266
|
+
// noch die Quelle vergessen, das entscheidet erst quelleAufraeumen unten.
|
|
267
|
+
const teile = [
|
|
268
|
+
zahlwort(inventar.links.length, 'Link', 'Links'),
|
|
269
|
+
zahlwort(inventar.checkouts.length, 'Checkout', 'Checkouts'),
|
|
270
|
+
];
|
|
271
|
+
const bleibt = (n) => (n === 1 ? ', bleibt liegen' : ', bleiben liegen');
|
|
272
|
+
if (inventar.plugins.length > 0)
|
|
273
|
+
teile.push(zahlwort(inventar.plugins.length, 'installiertes Plugin', 'installierte Plugins') + bleibt(inventar.plugins.length));
|
|
274
|
+
if (inventar.zeigerPfade.length > 0)
|
|
275
|
+
teile.push(zahlwort(inventar.zeigerPfade.length, 'Zeiger-Eintrag', 'Zeiger-Einträge') + bleibt(inventar.zeigerPfade.length));
|
|
276
|
+
let frage = `Skills von ${quelleAlsText(gefuehrtVorher)} komplett vom Rechner löschen (${teile.join(', ')})?`;
|
|
277
|
+
if (inventar.checkoutsMitLokalenAenderungen.length > 0) {
|
|
278
|
+
const anzahl = inventar.checkoutsMitLokalenAenderungen.length;
|
|
279
|
+
frage +=
|
|
280
|
+
`\n${anzahl === 1 ? 'Achtung: 1 Checkout hat' : `Achtung: ${anzahl} Checkouts haben`} lokale Änderungen. ` +
|
|
281
|
+
'Ein Ja bricht dann ab, ohne zu löschen, die Quelle bleibt bekannt. Erst committen/pushen oder verwerfen, dann erneut versuchen.';
|
|
282
|
+
}
|
|
283
|
+
if (!(await ports.rueckfrage.bestaetigen(frage))) {
|
|
284
|
+
const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
|
|
285
|
+
meldeVergessen(gefuehrt, inventar.links.length);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
// Ja: erst aufräumen (wirft bei lokalen Änderungen, VOR jeder Löschung, F31a), erst danach die
|
|
289
|
+
// Quelle vergessen, damit ein Abbruch sie unberührt lässt.
|
|
290
|
+
const ergebnis = await anwendungsfaelle.quelleAufraeumen(inventar);
|
|
291
|
+
const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
|
|
292
|
+
console.log(`Quelle ${quelleAlsText(gefuehrt)} entfernt und aufgeräumt.`);
|
|
293
|
+
console.log(` entfernt ${zahlwort(ergebnis.entfernteLinks, 'Link', 'Links')}, ${zahlwort(ergebnis.entfernteCheckouts, 'Checkout', 'Checkouts')}.`);
|
|
294
|
+
if (inventar.plugins.length > 0)
|
|
295
|
+
console.log(` installierte Plugins bleiben liegen: ${inventar.plugins.join(', ')}`);
|
|
296
|
+
if (inventar.zeigerPfade.length > 0)
|
|
297
|
+
console.log(` Zeiger-Einträge bleiben liegen: ${inventar.zeigerPfade.join(', ')}`);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
if (unterkommando === 'rename') {
|
|
301
|
+
const text = rest[1];
|
|
302
|
+
const name = rest[2];
|
|
303
|
+
if (text === undefined || name === undefined || name.trim().length === 0)
|
|
304
|
+
throw new Error('source rename braucht eine URL oder einen Pfad und einen neuen Namen, etwa hmgr source rename https://example.com/skills.git skills');
|
|
305
|
+
const genannt = quelleAusText(text);
|
|
306
|
+
const umbenannt = await anwendungsfaelle.quelleUmbenennen(genannt, name);
|
|
307
|
+
console.log(`Quelle ${quelleAlsText(umbenannt)} heißt jetzt „${quelleName(umbenannt)}".`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
204
310
|
if (unterkommando === 'list') {
|
|
205
311
|
const zustand = await ports.zustand.lesen();
|
|
206
312
|
if (zustand.quellen.length === 0)
|
|
207
313
|
console.log('Keine Quellen.');
|
|
208
314
|
for (const quelle of zustand.quellen)
|
|
209
|
-
console.log(`${quelle.art} ${quelleAlsText(quelle)}`);
|
|
315
|
+
console.log(`${quelle.art} ${quelleName(quelle)} ${quelleAlsText(quelle)}`);
|
|
210
316
|
return 0;
|
|
211
317
|
}
|
|
212
|
-
throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
|
|
318
|
+
throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad>, source rename <url|pfad> <name>, source remove <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
|
|
213
319
|
}
|
|
214
320
|
if (kommando === 'dir') {
|
|
215
321
|
const schalter = schalterLesen(argv.slice(1), 'dir');
|
|
@@ -269,8 +375,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
269
375
|
await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
|
|
270
376
|
}
|
|
271
377
|
if (kommando === 'unlink') {
|
|
272
|
-
await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
|
|
378
|
+
const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
|
|
273
379
|
console.log(`Link ${skill} entfernt.`);
|
|
380
|
+
if (ergebnis.hinweis !== undefined)
|
|
381
|
+
console.log(ergebnis.hinweis);
|
|
274
382
|
return 0;
|
|
275
383
|
}
|
|
276
384
|
const quelle = werte.get('quelle');
|
|
@@ -327,6 +435,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
327
435
|
}
|
|
328
436
|
const quellenText = neueQuellen === 1 ? '1 neue Quelle' : `${neueQuellen} neue Quellen`;
|
|
329
437
|
console.log(`${uebernommen} übernommen, ${uebersprungen} übersprungen, ${quellenText}.`);
|
|
438
|
+
if (ergebnis.hinweis !== undefined)
|
|
439
|
+
console.log(ergebnis.hinweis);
|
|
330
440
|
return 0;
|
|
331
441
|
}
|
|
332
442
|
if (kommando === 'plugin') {
|
|
@@ -380,6 +490,64 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
380
490
|
console.log(` ${zeile}`);
|
|
381
491
|
return 0;
|
|
382
492
|
}
|
|
493
|
+
if (kommando === 'mcp') {
|
|
494
|
+
const schalter = schalterLesen(argv.slice(1), 'mcp');
|
|
495
|
+
unterkommandoPruefen('mcp', schalter);
|
|
496
|
+
const { rest, werte } = schalter;
|
|
497
|
+
const unterkommando = rest[0];
|
|
498
|
+
if (unterkommando === 'list') {
|
|
499
|
+
const uebersicht = await anwendungsfaelle.mcpUebersicht();
|
|
500
|
+
if (uebersicht.katalog.length === 0)
|
|
501
|
+
console.log('Kein MCP im Katalog. hmgr help mcp zeigt das Format von ~/.hmgr/mcp.yaml.');
|
|
502
|
+
for (const { eintrag, eingehaengt, geteilteFundorte } of uebersicht.katalog) {
|
|
503
|
+
console.log(`${eintrag.name} ${eintrag.beschreibung}`);
|
|
504
|
+
if (eingehaengt.length === 0 && geteilteFundorte.length === 0)
|
|
505
|
+
console.log(' nirgends eingehängt');
|
|
506
|
+
for (const ziel of eingehaengt)
|
|
507
|
+
console.log(` ${ziel.harness} ${beschreibeOrt(ziel.ort)}`);
|
|
508
|
+
// Geteilte, nur lesbare Fundorte (E61-Nachtrag): eigens gekennzeichnet, hmgr fasst sie nicht an.
|
|
509
|
+
for (const f of geteilteFundorte)
|
|
510
|
+
console.log(` ${f.ziel.harness} ${beschreibeOrt(f.ziel.ort)} ${f.datei} · Scope ${f.scope} (geteilt, hmgr fasst sie nicht an)`);
|
|
511
|
+
}
|
|
512
|
+
if (uebersicht.unbekannt.length > 0 || uebersicht.unbekannteGeteilteFundorte.length > 0) {
|
|
513
|
+
console.log('Nicht im Katalog, aber eingehängt (hmgr fasst sie nicht an):');
|
|
514
|
+
for (const { ziel, name } of uebersicht.unbekannt)
|
|
515
|
+
console.log(` ${name} ${ziel.harness} ${beschreibeOrt(ziel.ort)}`);
|
|
516
|
+
for (const f of uebersicht.unbekannteGeteilteFundorte)
|
|
517
|
+
console.log(` ${f.name} ${f.ziel.harness} ${beschreibeOrt(f.ziel.ort)} ${f.datei} · Scope ${f.scope}`);
|
|
518
|
+
}
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'migrate')
|
|
522
|
+
throw new Error(`${unterkommando === undefined ? 'mcp braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für mcp`}: mcp add <name>, mcp remove <name>, mcp migrate <name> oder mcp list. hmgr help mcp zeigt Beispiele.`);
|
|
523
|
+
const name = rest[1];
|
|
524
|
+
if (name === undefined)
|
|
525
|
+
throw new Error(`mcp ${unterkommando} braucht einen MCP-Namen. hmgr mcp list zeigt die Katalog-Einträge.`);
|
|
526
|
+
const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
|
|
527
|
+
// Übernehmen (E64): der Name muss nicht im Katalog stehen, der Block kommt aus der geteilten Datei.
|
|
528
|
+
if (unterkommando === 'migrate') {
|
|
529
|
+
const ergebnis = await anwendungsfaelle.mcpUebernehmen({ name, ziel });
|
|
530
|
+
console.log(`MCP ${ergebnis.name} in den privaten Scope von ${ziel.harness} ${beschreibeOrt(ziel.ort)} übernommen `
|
|
531
|
+
+ `und aus ${ergebnis.datei} entfernt.`);
|
|
532
|
+
if (ergebnis.hinweis !== undefined)
|
|
533
|
+
console.log(` ${ergebnis.hinweis}`);
|
|
534
|
+
return 0;
|
|
535
|
+
}
|
|
536
|
+
if (unterkommando === 'add') {
|
|
537
|
+
const ergebnis = await anwendungsfaelle.mcpEinhaengen({ name, ziel });
|
|
538
|
+
console.log(`MCP ${ergebnis.eintrag.name} eingehängt in ${ziel.harness} ${beschreibeOrt(ziel.ort)}.`);
|
|
539
|
+
if (ergebnis.hinweis !== undefined)
|
|
540
|
+
console.log(` ${ergebnis.hinweis}`);
|
|
541
|
+
if (ergebnis.warnung !== undefined)
|
|
542
|
+
console.log(` ${ergebnis.warnung}`);
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
const ergebnis = await anwendungsfaelle.mcpAushaengen({ name, ziel });
|
|
546
|
+
console.log(`MCP ${ergebnis.eintrag.name} ausgehängt aus ${ziel.harness} ${beschreibeOrt(ziel.ort)}.`);
|
|
547
|
+
if (ergebnis.hinweis !== undefined)
|
|
548
|
+
console.log(` ${ergebnis.hinweis}`);
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
383
551
|
if (kommando === 'sync') {
|
|
384
552
|
const { rest } = schalterLesen(argv.slice(1), 'sync');
|
|
385
553
|
restPruefen(rest, 1, 'sync');
|
|
@@ -409,8 +577,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
409
577
|
console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
|
|
410
578
|
else if (aktualisierung.ergebnis === 'unveraendert')
|
|
411
579
|
console.log(` unverändert ${kopf}`);
|
|
412
|
-
else
|
|
580
|
+
else if (aktualisierung.ergebnis === 'uebersprungen')
|
|
413
581
|
console.log(` übersprungen ${kopf} lokale Änderung (E15)`);
|
|
582
|
+
else
|
|
583
|
+
console.log(` fehlgeschlagen ${kopf} ${aktualisierung.fehler}`);
|
|
414
584
|
}
|
|
415
585
|
for (const name of pluginHarnesses)
|
|
416
586
|
console.log(` Plugins ${name} aktualisiert (E14)`);
|
|
@@ -440,12 +610,12 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
440
610
|
if (eintrag.art === 'fremd')
|
|
441
611
|
console.log(` ${eintrag.name} fremd`);
|
|
442
612
|
else if (eintrag.art === 'link-ohne-quelle')
|
|
443
|
-
console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})`);
|
|
613
|
+
console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.name))}`);
|
|
444
614
|
else {
|
|
445
615
|
// Steht im Zeiger eine Versionseinschränkung, kommt sie vor den aufgelösten Ref (E33).
|
|
446
616
|
const einschraenkung = zielZustand.einschraenkungen.get(eintrag.link.name);
|
|
447
617
|
const spanne = einschraenkung === undefined ? '' : `${einschraenkung} → `;
|
|
448
|
-
console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})`);
|
|
618
|
+
console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.link.name))}`);
|
|
449
619
|
}
|
|
450
620
|
}
|
|
451
621
|
if (zielZustand.plugins !== undefined)
|
|
@@ -456,8 +626,132 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
456
626
|
console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
|
|
457
627
|
return 0;
|
|
458
628
|
}
|
|
629
|
+
if (kommando === 'harness') {
|
|
630
|
+
const schalter = schalterLesen(argv.slice(1), 'harness');
|
|
631
|
+
unterkommandoPruefen('harness', schalter);
|
|
632
|
+
const { rest, werte } = schalter;
|
|
633
|
+
const unterkommando = rest[0];
|
|
634
|
+
if (unterkommando === 'skills-dir') {
|
|
635
|
+
const harnessName = rest[1];
|
|
636
|
+
if (harnessName === undefined)
|
|
637
|
+
throw new Error('harness skills-dir braucht einen Harness-Namen, etwa hmgr harness skills-dir hermes. ' +
|
|
638
|
+
'hmgr harness list zeigt die bekannten Namen.');
|
|
639
|
+
const unter = rest[2];
|
|
640
|
+
const profilName = werte.get('profil');
|
|
641
|
+
const ort = profilName === undefined ? { art: 'home' } : { art: 'profil', name: profilName };
|
|
642
|
+
const ziel = { harness: harnessName, ort };
|
|
643
|
+
const ortText = beschreibeOrt(ort);
|
|
644
|
+
if (unter === undefined) {
|
|
645
|
+
restPruefen(rest, 2, 'harness', 'skills-dir');
|
|
646
|
+
const pfad = await anwendungsfaelle.verwalteterSkillOrdner(ziel);
|
|
647
|
+
if (pfad === undefined) {
|
|
648
|
+
console.log(`${harnessName} ${ortText}: kein Skill-Ordner verwaltet.`);
|
|
649
|
+
console.log(` Festlegen mit hmgr harness skills-dir ${harnessName} add <pfad>.`);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
console.log(`${harnessName} ${ortText} ${pfad}`);
|
|
653
|
+
}
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
if (unter === 'add') {
|
|
657
|
+
restPruefen(rest, 4, 'harness', 'skills-dir');
|
|
658
|
+
const pfad = rest[3];
|
|
659
|
+
if (pfad === undefined)
|
|
660
|
+
throw new Error('harness skills-dir <name> add braucht einen Pfad, etwa ' +
|
|
661
|
+
'hmgr harness skills-dir hermes add ~/.hermes/external-skills.');
|
|
662
|
+
const gesetzt = await anwendungsfaelle.skillOrdnerFestlegen(ziel, pfad);
|
|
663
|
+
console.log(`${harnessName} ${ortText} liest Skills jetzt aus ${gesetzt}.`);
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
if (unter === 'remove') {
|
|
667
|
+
restPruefen(rest, 3, 'harness', 'skills-dir');
|
|
668
|
+
const ergebnis = await anwendungsfaelle.skillOrdnerEntfernen(ziel);
|
|
669
|
+
console.log(`${harnessName} ${ortText}: Eintrag für ${ergebnis.pfad} entfernt.`);
|
|
670
|
+
const inhalt = `${zahlwort(ergebnis.hmgrLinks, 'hmgr-Link', 'hmgr-Links')}, ${zahlwort(ergebnis.fremdeEintraege, 'fremder Eintrag', 'fremde Einträge')}`;
|
|
671
|
+
const frage = `Ordner ${ergebnis.pfad} enthält noch ${inhalt}. Löschen?`;
|
|
672
|
+
if (await ports.rueckfrage.bestaetigen(frage)) {
|
|
673
|
+
await anwendungsfaelle.skillOrdnerLoeschen(ziel, ergebnis.pfad);
|
|
674
|
+
console.log(` Ordner ${ergebnis.pfad} gelöscht.`);
|
|
675
|
+
}
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
throw new Error(`Unbekanntes Unterkommando „${unter}" für harness skills-dir: harness skills-dir <name>, ` +
|
|
679
|
+
'harness skills-dir <name> add <pfad> oder harness skills-dir <name> remove. ' +
|
|
680
|
+
'hmgr help harness zeigt Beispiele.');
|
|
681
|
+
}
|
|
682
|
+
if (unterkommando === 'list') {
|
|
683
|
+
const aktive = new Set(await anwendungsfaelle.aktiveHarnesses());
|
|
684
|
+
const fehlend = new Set(await anwendungsfaelle.nichtVorhandeneHarnesses());
|
|
685
|
+
for (const name of anwendungsfaelle.harnessTypen()) {
|
|
686
|
+
const aktivText = aktive.has(name) ? 'aktiv' : 'inaktiv';
|
|
687
|
+
const vorhandenText = fehlend.has(name) ? 'nicht vorhanden' : 'vorhanden';
|
|
688
|
+
console.log(`${name} ${aktivText}, ${vorhandenText}`);
|
|
689
|
+
}
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
if (unterkommando === 'enable' || unterkommando === 'disable') {
|
|
693
|
+
const name = rest[1];
|
|
694
|
+
if (name === undefined)
|
|
695
|
+
throw new Error(`harness ${unterkommando} braucht einen Harness-Namen. hmgr harness list zeigt die bekannten Harness-Typen.`);
|
|
696
|
+
const vorherAktiv = new Set(await anwendungsfaelle.aktiveHarnesses());
|
|
697
|
+
if (unterkommando === 'enable') {
|
|
698
|
+
await anwendungsfaelle.harnessAktivieren(name);
|
|
699
|
+
console.log(vorherAktiv.has(name) ? `${name} ist schon aktiv.` : `${name} aktiviert.`);
|
|
700
|
+
return 0;
|
|
701
|
+
}
|
|
702
|
+
await anwendungsfaelle.harnessDeaktivieren(name);
|
|
703
|
+
console.log(vorherAktiv.has(name) ? `${name} deaktiviert.` : `${name} ist schon inaktiv.`);
|
|
704
|
+
// Säubern-Angebot nur, wenn eigene Konfiguration liegt, mit derselben Bedingung wie die GUI
|
|
705
|
+
// (E38, GRILL.md Entwurfskritik 4): eigene Links oder eine liegen gebliebene Registrierung
|
|
706
|
+
// ohne Link (E38 R7), sonst machten CLI und GUI verschiedene Angebote.
|
|
707
|
+
const ziele = (await anwendungsfaelle.istZustand()).filter((z) => z.ziel.harness === name &&
|
|
708
|
+
(z.eintraege.some((eintrag) => eintrag.art !== 'fremd') || z.istRegistriert));
|
|
709
|
+
if (ziele.length === 0)
|
|
710
|
+
return 0;
|
|
711
|
+
const links = ziele.reduce((n, z) => n + z.eintraege.filter((eintrag) => eintrag.art !== 'fremd').length, 0);
|
|
712
|
+
const eintraege = ziele.filter((z) => z.istRegistriert).length;
|
|
713
|
+
// Wer den harness-eigenen Eintrag angelegt hat, weiß niemand (E38 R7), also wird er benannt,
|
|
714
|
+
// nicht hmgr zugeschrieben. Gleicher Text wie bilanzText in der GUI.
|
|
715
|
+
const teile = [
|
|
716
|
+
links === 0
|
|
717
|
+
? zahlwort(ziele.length, 'leeres Ziel', 'leere Ziele')
|
|
718
|
+
: `${zahlwort(links, 'Link', 'Links')} in ${zahlwort(ziele.length, 'Ziel', 'Zielen')}`,
|
|
719
|
+
];
|
|
720
|
+
if (eintraege > 0)
|
|
721
|
+
teile.push(zahlwort(eintraege, 'harness-eigener Eintrag', 'harness-eigene Einträge'));
|
|
722
|
+
const liste = ziele.map((z) => ` ${z.verzeichnis}`).join('\n');
|
|
723
|
+
const frage = `Es liegt noch Konfiguration (${teile.join(', ')}):\n${liste}\nEntfernen?`;
|
|
724
|
+
if (!(await ports.rueckfrage.bestaetigen(frage))) {
|
|
725
|
+
console.log(' Konfiguration bleibt liegen, hmgr pflegt sie nicht mehr.');
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
const ergebnis = await anwendungsfaelle.saeubern(name);
|
|
729
|
+
for (const zielErgebnis of ergebnis.ziele) {
|
|
730
|
+
if (zielErgebnis.entfernteLinks.length > 0)
|
|
731
|
+
console.log(` entfernt ${beschreibeOrt(zielErgebnis.ziel.ort)} ${zielErgebnis.entfernteLinks.join(', ')}`);
|
|
732
|
+
if (zielErgebnis.konfigurationZurueckgebaut)
|
|
733
|
+
console.log(` zurückgebaut ${beschreibeOrt(zielErgebnis.ziel.ort)} harness-eigene Konfiguration`);
|
|
734
|
+
}
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
throw new Error(`${unterkommando === undefined ? 'harness braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für harness`}: harness list, harness enable <name> oder harness disable <name>. hmgr help harness zeigt Beispiele.`);
|
|
738
|
+
}
|
|
459
739
|
throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
|
|
460
740
|
}
|
|
741
|
+
/** Grund eines Links als Suffix (E37). „manuell" bleibt stumm, damit die Ausgabe ruhig bleibt. */
|
|
742
|
+
function beschreibeGrund(grund) {
|
|
743
|
+
if (grund === undefined || grund.art === 'manuell')
|
|
744
|
+
return '';
|
|
745
|
+
return grund.art === 'plugin' ? ` Plugin „${grund.plugin}"` : ' über Zeiger';
|
|
746
|
+
}
|
|
747
|
+
/** Zahl mit passender Endung, Singular oder Plural, für die Säubern-Frage bei harness disable (E38). */
|
|
748
|
+
function zahlwort(n, ein, viele) {
|
|
749
|
+
return `${n} ${n === 1 ? ein : viele}`;
|
|
750
|
+
}
|
|
751
|
+
/** Text auf `laenge` Zeichen kappen, mit „…" markiert, damit eine Skill-Liste lesbar bleibt (E39). */
|
|
752
|
+
function kappen(text, laenge = 80) {
|
|
753
|
+
return text.length <= laenge ? text : `${text.slice(0, laenge - 1)}…`;
|
|
754
|
+
}
|
|
461
755
|
function beschreibeOrt(ort) {
|
|
462
756
|
if (ort.art === 'home')
|
|
463
757
|
return 'home';
|
|
@@ -467,16 +761,35 @@ function beschreibeOrt(ort) {
|
|
|
467
761
|
}
|
|
468
762
|
function istModellFehler(fehler) {
|
|
469
763
|
return (fehler instanceof NameBelegt ||
|
|
764
|
+
fehler instanceof LinkAnlegenFehlgeschlagen ||
|
|
470
765
|
fehler instanceof SkillMehrdeutig ||
|
|
471
766
|
fehler instanceof SkillUnbekannt ||
|
|
472
767
|
fehler instanceof OrtNichtUnterstuetzt ||
|
|
473
768
|
fehler instanceof ZeigerMehrdeutig ||
|
|
769
|
+
fehler instanceof ZeigerSchreibenGesperrt ||
|
|
474
770
|
fehler instanceof HarnessNichtVorhanden ||
|
|
771
|
+
fehler instanceof HarnessTypUnbekannt ||
|
|
475
772
|
fehler instanceof KeinPassenderRef ||
|
|
476
773
|
fehler instanceof EinschraenkungOhneZeiger ||
|
|
477
774
|
fehler instanceof PluginMehrdeutig ||
|
|
478
775
|
fehler instanceof PluginUnbekannt ||
|
|
479
|
-
fehler instanceof AbhaengigkeitFehlt
|
|
776
|
+
fehler instanceof AbhaengigkeitFehlt ||
|
|
777
|
+
fehler instanceof QuelleUnbekannt ||
|
|
778
|
+
fehler instanceof QuelleNameBelegt ||
|
|
779
|
+
fehler instanceof QuelleAufraeumenLokaleAenderungen ||
|
|
780
|
+
fehler instanceof SkillOrdnerFaehigkeitFehlt ||
|
|
781
|
+
fehler instanceof SkillOrdnerBereitsVerwaltet ||
|
|
782
|
+
fehler instanceof SkillOrdnerNichtVerwaltet ||
|
|
783
|
+
fehler instanceof SkillOrdnerUngueltig ||
|
|
784
|
+
fehler instanceof McpUnbekannt ||
|
|
785
|
+
fehler instanceof McpFaehigkeitFehlt ||
|
|
786
|
+
fehler instanceof McpEbeneNichtUnterstuetzt ||
|
|
787
|
+
fehler instanceof McpAbweichenderEintrag ||
|
|
788
|
+
fehler instanceof McpKatalogFehler ||
|
|
789
|
+
fehler instanceof McpNurGeteilterFundort ||
|
|
790
|
+
fehler instanceof McpUebernahmeNichtMoeglich ||
|
|
791
|
+
fehler instanceof McpNichtInGeteilterDatei ||
|
|
792
|
+
fehler instanceof McpUebernahmeUnvollstaendig);
|
|
480
793
|
}
|
|
481
794
|
/**
|
|
482
795
|
* Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
|
|
@@ -501,9 +814,38 @@ function naechsterSchritt(fehler) {
|
|
|
501
814
|
return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
|
|
502
815
|
if (fehler instanceof OrtNichtUnterstuetzt)
|
|
503
816
|
return 'profil:<name> kennt nur hermes, claude-code und agents kennen home und dir:<pfad>.';
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
817
|
+
if (fehler instanceof HarnessTypUnbekannt)
|
|
818
|
+
return 'Bekannt sind claude-code, hermes und agents. hmgr harness list zeigt sie mit ihrem Stand.';
|
|
819
|
+
if (fehler instanceof QuelleUnbekannt)
|
|
820
|
+
return 'hmgr source list zeigt die bekannten Quellen.';
|
|
821
|
+
if (fehler instanceof QuelleNameBelegt)
|
|
822
|
+
return 'hmgr source list zeigt die schon vergebenen Namen.';
|
|
823
|
+
if (fehler instanceof ZeigerSchreibenGesperrt)
|
|
824
|
+
return 'Nutze --local, das schreibt nichts in den Zeiger.';
|
|
825
|
+
if (fehler instanceof SkillOrdnerFaehigkeitFehlt)
|
|
826
|
+
return 'Nur hermes kennt einen verwalteten Skill-Ordner.';
|
|
827
|
+
if (fehler instanceof SkillOrdnerBereitsVerwaltet)
|
|
828
|
+
return `Erst entfernen: hmgr harness skills-dir ${fehler.ziel.harness} remove${fehler.ziel.ort.art === 'profil' ? ` --profil ${fehler.ziel.ort.name}` : ''}.`;
|
|
829
|
+
if (fehler instanceof SkillOrdnerNichtVerwaltet)
|
|
830
|
+
return `Festlegen mit hmgr harness skills-dir ${fehler.ziel.harness} add <pfad>${fehler.ziel.ort.art === 'profil' ? ` --profil ${fehler.ziel.ort.name}` : ''}.`;
|
|
831
|
+
if (fehler instanceof SkillOrdnerUngueltig)
|
|
832
|
+
return fehler.grund === 'ausserhalb'
|
|
833
|
+
? 'Der Pfad muss unterhalb des Hermes-Home liegen.'
|
|
834
|
+
: 'Das ist Hermes\' eigener Skill-Ordner, ein anderer Pfad muss her.';
|
|
835
|
+
if (fehler instanceof McpUnbekannt)
|
|
836
|
+
return 'hmgr mcp list zeigt die Katalog-Einträge. Gepflegt wird der Katalog in ~/.hmgr/mcp.yaml, hmgr help mcp zeigt das Format.';
|
|
837
|
+
if (fehler instanceof McpFaehigkeitFehlt)
|
|
838
|
+
return 'Nur claude-code und hermes kennen MCP-Server.';
|
|
839
|
+
if (fehler instanceof McpEbeneNichtUnterstuetzt)
|
|
840
|
+
return 'claude-code kennt --ort home und --ort dir:<pfad>, hermes --ort home und --ort profil:<name>.';
|
|
841
|
+
if (fehler instanceof McpAbweichenderEintrag)
|
|
842
|
+
return 'Erst von Hand angleichen oder mit hmgr mcp remove entfernen, dann erneut versuchen.';
|
|
843
|
+
if (fehler instanceof McpKatalogFehler)
|
|
844
|
+
return '~/.hmgr/mcp.yaml prüfen und korrigieren, hmgr help mcp zeigt das Format.';
|
|
845
|
+
if (fehler instanceof McpUebernahmeNichtMoeglich)
|
|
846
|
+
return 'Eine geteilte .mcp.json kennt nur claude-code, und nur an einem Arbeitsverzeichnis: --harness claude-code --ort dir:<pfad>.';
|
|
847
|
+
if (fehler instanceof McpNichtInGeteilterDatei)
|
|
848
|
+
return 'hmgr mcp list zeigt, welche Namen dort stehen; mcp migrate übernimmt nur Einträge aus der geteilten Datei.';
|
|
507
849
|
return undefined;
|
|
508
850
|
}
|
|
509
851
|
export async function main(argv, umgebung) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Datei-Watcher für externe Änderungen (E62): main.ts verdrahtet nur, die eigentliche Logik ist
|
|
2
|
+
// hier rein (kein Electron-Import), nach dem Muster von oeffnen.ts und icons.ts, damit sie ohne
|
|
3
|
+
// Electron testbar ist.
|
|
4
|
+
//
|
|
5
|
+
// Beobachtet wird eine feste Liste von Pfaden (hmgr-Zustand, MCP-Katalog, Harness-MCP-Konfigs,
|
|
6
|
+
// .env-Dateien, siehe main.ts). Zwei Eigenheiten des Dateisystems bestimmen den Aufbau, beide am
|
|
7
|
+
// laufenden System gemessen (macOS 25, Node 22):
|
|
8
|
+
//
|
|
9
|
+
// 1. Beobachtet wird nie die Datei selbst, sondern immer ihr Elternverzeichnis mit einem Filter auf
|
|
10
|
+
// die Dateinamen darin. `fs.watch` auf eine Datei hängt am Inode und wird nach dem ersten
|
|
11
|
+
// atomaren Ersetzen (schreiben in eine Temp-Datei, dann `rename` darüber) dauerhaft taub — kein
|
|
12
|
+
// weiteres Event, weder bei erneutem `rename` noch beim Schreiben an Ort und Stelle. Genau so
|
|
13
|
+
// schreiben viele Editoren, und so wird auch `~/.claude.json` ersetzt. Ein Watcher auf das
|
|
14
|
+
// Verzeichnis überlebt das und meldet den Dateinamen mit. Nebenbei deckt er Dateien ab, die beim
|
|
15
|
+
// Start noch gar nicht existieren: tauchen sie später auf, fängt derselbe Watcher es ab.
|
|
16
|
+
//
|
|
17
|
+
// 2. Ein Event heißt nicht, dass sich etwas geändert hat. `claude plugin list` — das hmgr bei jedem
|
|
18
|
+
// Anzeige-Aufbau je Claude-Code-Ziel ruft — ersetzt `~/.claude.json` jedes Mal per `rename`, mit
|
|
19
|
+
// unverändertem Inhalt. Ohne Gegenmaßnahme entstünde daraus eine Endlosschleife: Anzeige laden →
|
|
20
|
+
// Datei ersetzt → Watcher feuert → Anzeige laden. Darum stößt der Watcher erst an, wenn sich der
|
|
21
|
+
// Inhalt mindestens einer beobachteten Datei gegenüber dem letzten Stand wirklich unterscheidet.
|
|
22
|
+
//
|
|
23
|
+
// Fehlt das Elternverzeichnis, bleibt der Pfad unbeobachtet, bis ihn der nächste Fokus-Reload (E62)
|
|
24
|
+
// ohnehin erfasst. Mehrere Events in kurzer Folge (ein Editor schreibt beim Speichern oft mehrfach,
|
|
25
|
+
// ein `rename` meldet mehrere Events) lösen genau einen Callback aus: jedes Event verschiebt einen
|
|
26
|
+
// Timer um `entprellenMs` nach hinten, erst danach wird verglichen.
|
|
27
|
+
import { readFileSync, watch } from 'node:fs';
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
const STANDARD_ENTPRELLEN_MS = 400;
|
|
31
|
+
/** Marke für eine Datei, die es (noch) nicht gibt — unterscheidbar von jedem echten Inhalt. */
|
|
32
|
+
const FEHLT = 'fehlt';
|
|
33
|
+
/** Fingerabdruck des Inhalts. Unlesbar (fehlend, keine Rechte) zählt als `FEHLT`, nicht als Änderung. */
|
|
34
|
+
function fingerabdruck(pfad) {
|
|
35
|
+
try {
|
|
36
|
+
return createHash('sha256').update(readFileSync(pfad)).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return FEHLT;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Beobachtet `pfade` auf inhaltliche Änderungen und ruft `callback` entprellt auf. Doppelte Pfade
|
|
44
|
+
* werden nur einmal beobachtet, Pfade im selben Verzeichnis teilen sich einen Watcher.
|
|
45
|
+
*/
|
|
46
|
+
export function beobachten(pfade, callback, optionen = {}) {
|
|
47
|
+
const entprellenMs = optionen.entprellenMs ?? STANDARD_ENTPRELLEN_MS;
|
|
48
|
+
const watcher = [];
|
|
49
|
+
let timer;
|
|
50
|
+
let geschlossen = false;
|
|
51
|
+
const eindeutige = [...new Set(pfade)];
|
|
52
|
+
const stand = new Map(eindeutige.map((p) => [p, fingerabdruck(p)]));
|
|
53
|
+
/** Zieht den Stand nach und sagt, ob sich dabei etwas geändert hat. */
|
|
54
|
+
function hatSichGeaendert() {
|
|
55
|
+
let geaendert = false;
|
|
56
|
+
for (const pfad of eindeutige) {
|
|
57
|
+
const neu = fingerabdruck(pfad);
|
|
58
|
+
if (stand.get(pfad) !== neu) {
|
|
59
|
+
stand.set(pfad, neu);
|
|
60
|
+
geaendert = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return geaendert;
|
|
64
|
+
}
|
|
65
|
+
function anstossen() {
|
|
66
|
+
if (geschlossen)
|
|
67
|
+
return;
|
|
68
|
+
if (timer !== undefined)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
timer = setTimeout(() => {
|
|
71
|
+
timer = undefined;
|
|
72
|
+
if (geschlossen)
|
|
73
|
+
return;
|
|
74
|
+
if (hatSichGeaendert())
|
|
75
|
+
callback();
|
|
76
|
+
}, entprellenMs);
|
|
77
|
+
}
|
|
78
|
+
// Je Verzeichnis ein Watcher, der die dort gesuchten Dateinamen kennt.
|
|
79
|
+
const jeVerzeichnis = new Map();
|
|
80
|
+
for (const pfad of eindeutige) {
|
|
81
|
+
const verzeichnis = path.dirname(pfad);
|
|
82
|
+
const namen = jeVerzeichnis.get(verzeichnis) ?? new Set();
|
|
83
|
+
namen.add(path.basename(pfad));
|
|
84
|
+
jeVerzeichnis.set(verzeichnis, namen);
|
|
85
|
+
}
|
|
86
|
+
for (const [verzeichnis, namen] of jeVerzeichnis) {
|
|
87
|
+
try {
|
|
88
|
+
watcher.push(watch(verzeichnis, (_ereignis, geaenderterName) => {
|
|
89
|
+
// Ohne Namen (kommt vor, je nach Plattform) wird konservativ angestoßen; der
|
|
90
|
+
// Inhaltsvergleich entscheidet ohnehin.
|
|
91
|
+
if (geaenderterName === null || namen.has(path.basename(geaenderterName)))
|
|
92
|
+
anstossen();
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Das Verzeichnis fehlt: diese Pfade bleiben unbeobachtet (siehe Kommentar oben).
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
close() {
|
|
101
|
+
geschlossen = true;
|
|
102
|
+
if (timer !== undefined)
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
for (const w of watcher)
|
|
105
|
+
w.close();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|