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.
Files changed (41) hide show
  1. package/README.md +10 -0
  2. package/dist/adapter/checkout-pfad.js +10 -2
  3. package/dist/adapter/claude-code.js +251 -23
  4. package/dist/adapter/composer.js +4 -3
  5. package/dist/adapter/datei-text.js +18 -0
  6. package/dist/adapter/dateisystem.js +146 -8
  7. package/dist/adapter/git.js +129 -17
  8. package/dist/adapter/hermes.js +235 -10
  9. package/dist/adapter/marketplace-json.js +2 -1
  10. package/dist/adapter/mcp-katalog.js +124 -0
  11. package/dist/adapter/quelle-text.js +10 -1
  12. package/dist/adapter/skill-md.js +96 -19
  13. package/dist/adapter/terminal.js +1 -1
  14. package/dist/adapter/umgebung.js +43 -0
  15. package/dist/adapter/vergleich.js +20 -0
  16. package/dist/adapter/zustand-json.js +6 -1
  17. package/dist/cli/hilfe.js +227 -26
  18. package/dist/cli/main.js +362 -20
  19. package/dist/gui/beobachter.js +108 -0
  20. package/dist/gui/icons.js +114 -0
  21. package/dist/gui/login-pfad.js +120 -0
  22. package/dist/gui/main.js +823 -0
  23. package/dist/gui/mcp-ansicht.js +57 -0
  24. package/dist/gui/oeffnen.js +33 -0
  25. package/dist/gui/preload.cjs +70 -0
  26. package/dist/gui/renderer/fonts/Inter-LICENSE.txt +92 -0
  27. package/dist/gui/renderer/fonts/InterVariable.woff2 +0 -0
  28. package/dist/gui/renderer/fonts/JetBrainsMono-Medium.woff2 +0 -0
  29. package/dist/gui/renderer/fonts/JetBrainsMono-OFL.txt +93 -0
  30. package/dist/gui/renderer/fonts/JetBrainsMono-Regular.woff2 +0 -0
  31. package/dist/gui/renderer/index.html +581 -0
  32. package/dist/gui/renderer/renderer.js +3227 -0
  33. package/dist/gui/renderer/styles.css +701 -0
  34. package/dist/kern/anwendungsfaelle.js +859 -65
  35. package/dist/kern/modell.js +297 -0
  36. package/dist/kern/quelle-ort.js +14 -1
  37. package/dist/ports/harness.js +16 -0
  38. package/dist/ports/mcp-katalog.js +1 -0
  39. package/dist/ports/umgebung.js +1 -0
  40. package/dist/ports/zustand.js +1 -1
  41. 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
- return { home, hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr') };
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: { add: { woerter: 2 }, list: { woerter: 1 } },
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.rest;
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
- console.log(` ${skill.name} ${skill.pfad}`);
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
- const text = fehler instanceof Error ? fehler.message : '';
505
- if (text.startsWith('Unbekannter Harness'))
506
- return 'Bekannt sind claude-code, hermes und agents.';
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
+ }