hmgr 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/hilfe.js CHANGED
@@ -22,6 +22,14 @@ hmgr <kommando> [schalter]
22
22
  plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
23
23
  plugin remove <name> Plugin aus einem Ziel entfernen
24
24
  plugin list Plugins der Marketplaces aller Quellen
25
+ bundle add <name> Bundle als Ganzes in ein Ziel installieren
26
+ bundle remove <name> Bundle als Ganzes aus einem Ziel entfernen
27
+ bundle sync [name] Installierte Bundles gegen ihre Definition ziehen
28
+ bundle list Bundles der bekannten Quellen
29
+ mcp add <name> MCP-Server aus dem Katalog in ein Ziel einhängen
30
+ mcp remove <name> MCP-Server aus einem Ziel aushängen
31
+ mcp migrate <name> MCP-Server aus der geteilten .mcp.json in den privaten Scope übernehmen
32
+ mcp list Katalog-Einträge zeigen, mit wo sie eingehängt sind
25
33
  sync [pfad] Ziel auf den Stand seines Zeigers bringen
26
34
  update Alle Checkouts aktualisieren
27
35
  status Ist-Zustand der Ziele
@@ -29,6 +37,7 @@ hmgr <kommando> [schalter]
29
37
  harness enable <name> Harness-Typ aktivieren, hmgr richtet ihn künftig ein
30
38
  harness disable <name> Harness-Typ deaktivieren, mit Angebot zum Säubern
31
39
  harness skills-dir <name> Verwalteten Skill-Ordner zeigen, festlegen oder entfernen
40
+ --version Versionsnummer von hmgr ausgeben
32
41
 
33
42
  hmgr help <kommando> zeigt Schalter, Beispiele und Zusammenhänge.`;
34
43
  const SOURCE = `hmgr source add <url|pfad> [--name <name>]
@@ -226,6 +235,180 @@ Häufige Fälle:
226
235
  kann nur die Quelle beheben.
227
236
 
228
237
  Siehe auch: hmgr help source, hmgr help sync`;
238
+ const BUNDLE = `hmgr bundle add <name> [schalter]
239
+ hmgr bundle remove <name> [schalter]
240
+ hmgr bundle sync [name] [schalter]
241
+ hmgr bundle list
242
+
243
+ Ein Bundle ist eine benannte Zusammenstellung von Skills über Quellen hinweg: was für ein Vorhaben
244
+ zusammengehört, liegt selten in einem Repo. Anders als ein Plugin (das lebt in einem Marketplace und
245
+ löst Abhängigkeiten nur dort auf) darf ein Bundle Skills aus beliebig vielen Quellen nennen.
246
+
247
+ Definiert wird es von Hand, als YAML-Datei in einer Quelle, Konvention bundles/<name>.yaml. hmgr
248
+ liest und validiert sie, schreibt sie nie — über die Quelle kommt sie versioniert auf alle
249
+ Maschinen. Es gibt keinen eigenen Katalog und keinen Pflegebefehl: bundle list zeigt, was beim
250
+ Quellen-Scan gefunden wurde.
251
+
252
+ Format der Datei:
253
+
254
+ name optional, sonst gilt der Dateiname ohne Endung
255
+ description Pflicht, kurzer Text
256
+ skills Pflicht, nicht leere Liste; je Eintrag source (URL oder Pfad der Quelle)
257
+ und path (Pfad des Skill-Ordners in dieser Quelle)
258
+
259
+ Beispiel bundles/wordpress.yaml:
260
+
261
+ name: wordpress
262
+ description: Alles für die WordPress-Arbeit
263
+ skills:
264
+ - source: git@gitea.example.com:tim/skills.git
265
+ path: skills/wp-core
266
+ - source: https://github.com/taquiri/skills
267
+ path: skills/tq-mailform
268
+
269
+ Ein Ref je Mitglied kennt die Definition nicht: der Link folgt dem Ref, mit dem die Quelle
270
+ eingerichtet ist. Angesprochen wird der Skill über seinen Pfad, nicht über seinen Namen — der Name
271
+ steht im Frontmatter und wird erst beim Lesen der Quelle bekannt.
272
+
273
+ Je Ziel ist das Bundle eine Einheit: add legt alle seine Links, remove nimmt alle wieder weg, sync
274
+ zieht Änderungen an der Definition nach. Nennt die Definition eine Quelle, die hmgr noch nicht
275
+ kennt, legt add sie an und sagt, welche neu sind. Lässt sich auch nur ein Mitglied nicht auflösen
276
+ oder liegt am Ziel ein Namenskonflikt, bricht add ab, bevor irgendetwas verlinkt oder eine Quelle
277
+ angelegt ist — ein halbes Bundle wäre stilles Fehlen von Skills. Wer nur einen Teil braucht,
278
+ verlinkt die erreichbaren Skills einzeln mit hmgr link.
279
+
280
+ Liegt am Ziel schon derselbe Skill aus derselben Quelle als hmgr-Link, wird er als Mitglied
281
+ übernommen: aus Bestand wird ein Bundle, ohne erst Links von Hand zu entfernen. Alles andere unter
282
+ demselben Namen ist ein Konflikt und wird genannt.
283
+
284
+ hmgr unlink <skill> auf ein einzelnes Mitglied zerbricht das Bundle in diesem Ziel: die übrigen
285
+ Links bleiben als einzelne Links liegen, der Bundle-Verweis fällt weg. Das ist kein Fehler, hmgr
286
+ sagt es nur.
287
+
288
+ Den Abgleich macht schon hmgr update für alle installierten Bundles mit, es ist derselbe Vorgang
289
+ und dieselbe Ausgabe (E69): eine geänderte Definition kommt also mit dem gewöhnlichen
290
+ Aktualisieren an, ohne dass man von der Änderung wissen müsste. bundle sync ist der gezielte Weg
291
+ daneben, wenn nur ein Bundle oder nur ein Ziel gemeint ist; ohne Namen und ohne Ziel-Schalter geht
292
+ es denselben Bestand durch wie update. Was sich nicht vollständig nachziehen lässt, bleibt in
293
+ seinem Ziel auf dem alten Stand und wird als fehlgeschlagen gemeldet.
294
+
295
+ Der Zeiger bleibt außen vor: die Zugehörigkeit der Links führt hmgr in seinem Zustand, nicht in der
296
+ composer.json. --local gibt es deshalb nicht, und die Sperre aus E57 trifft bundle nie.
297
+
298
+ Schalter (add, remove und sync):
299
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
300
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
301
+ --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
302
+ --quelle <teil> nur bei add: Quelle eingrenzen, wenn den Namen mehrere Quellen führen
303
+
304
+ Beispiele:
305
+ hmgr bundle list
306
+ hmgr bundle add wordpress --ort dir:.
307
+ hmgr bundle sync
308
+ hmgr bundle remove wordpress --ort dir:.
309
+
310
+ Häufige Fälle:
311
+ „steht in keiner Quelle": hmgr bundle list zeigt die bekannten Namen, sonst erst hmgr source add.
312
+ „liegt in mehreren Quellen": mit --quelle <teilstring> entscheiden.
313
+ „ist auf dieser Maschine nicht vollständig herstellbar": die Meldung nennt je Mitglied den Grund,
314
+ meist eine Quelle ohne Zugriff oder ein Pfad, an dem kein Skill (mehr) liegt.
315
+ „lässt sich in … nicht anlegen": die Meldung listet alle Namen, die am Ziel schon belegt sind.
316
+ „kein Bundle … installiert": hmgr status zeigt je Ziel seine Bundles.
317
+
318
+ Siehe auch: hmgr help link, hmgr help plugin, hmgr help status`;
319
+ const MCP = `hmgr mcp add <name> [schalter]
320
+ hmgr mcp remove <name> [schalter]
321
+ hmgr mcp migrate <name> [schalter]
322
+ hmgr mcp list
323
+
324
+ Ein MCP-Server ist ein Eintrag im persönlichen Katalog ~/.hmgr/mcp.yaml, nicht in einer Quelle: kein
325
+ Download, keine Installation, kein Update. hmgr liest den Katalog nur, er wird von Hand oder von
326
+ einem LLM gepflegt, das den MCP ohnehin gerade einrichtet. mcp add hängt einen Katalog-Eintrag in
327
+ ein Ziel ein (Projekt oder global), mcp remove hängt ihn wieder aus, mcp list zeigt den Katalog mit
328
+ je Eintrag, wo er eingehängt ist, dazu Einträge, die in Harness-Konfigurationen liegen, aber nicht
329
+ im Katalog stehen (nur genannt, hmgr fasst sie nicht an). mcp list sieht dabei in den Homes und
330
+ Profilen der Harnesses nach und in den Arbeitsverzeichnissen, die hmgr kennt (hmgr dir add); ein
331
+ MCP in einem Verzeichnis, das hmgr nicht kennt, taucht dort nicht auf.
332
+
333
+ Format von ~/.hmgr/mcp.yaml: ein Objekt, Schlüssel ist der MCP-Name. Je Eintrag:
334
+
335
+ beschreibung Pflicht, kurzer Text
336
+ homepage optional, URL
337
+ notizen optional, mehrzeilig
338
+ konfig Pflicht: entweder command (Pflicht), optional args und env,
339
+ oder url (Pflicht), optional headers
340
+
341
+ Andere Schlüssel im konfig-Block sind ein Fehler, mit Eintrag und Feld in der Meldung. Secrets
342
+ stehen als \${VAR}- oder \${env:VAR}-Verweise im Block, nie im Klartext; aufgelöst werden sie vom
343
+ Harness gegen dessen Umgebung, hmgr prüft beim Einhängen nur, ob die Variablen gesetzt sind, und
344
+ warnt sonst. Fehlt die Datei, ist der Katalog leer, kein Fehler; kaputtes YAML ist ein Fehler mit
345
+ dem Dateipfad.
346
+
347
+ Beispiel:
348
+
349
+ teamwork:
350
+ beschreibung: Teamwork.com Projektmanagement
351
+ homepage: https://teamwork.com
352
+ notizen: |
353
+ Erfordert TEAMWORK_API_KEY, siehe Teamwork-Profil.
354
+ konfig:
355
+ command: npx
356
+ args: ["-y", "@teamwork/mcp-server"]
357
+ env:
358
+ TEAMWORK_API_KEY: \${TEAMWORK_API_KEY}
359
+
360
+ context7:
361
+ beschreibung: Context7 Doku-Suche
362
+ konfig:
363
+ url: https://mcp.context7.com/mcp
364
+ headers:
365
+ Authorization: Bearer \${env:CONTEXT7_TOKEN}
366
+
367
+ Die Fähigkeit hängt am Harness-Adapter, nicht am Namen (E16): claude-code kennt Projekt (Scope
368
+ local, nie die geteilte .mcp.json der Projektwurzel, das bleibt Team-Sache wie beim Zeiger, E57) und
369
+ global (Scope user). Hermes kennt nur global, mcp_servers in seiner config.yaml (Home oder Profil
370
+ mit --ort profil:<name>); ein Projekt-Ziel ist bei Hermes ein Fehler, es kennt keine Projektebene für
371
+ MCPs. agents kennt MCP-Server gar nicht.
372
+
373
+ mcp migrate ist der einzige Weg, auf dem hmgr die geteilte .mcp.json anfasst (E64): der Eintrag
374
+ wandert 1:1 aus der Datei in deinen privaten Scope am selben Ort und wird dort gelöscht. Auch ein
375
+ Eintrag, der nicht im Katalog steht, lässt sich so übernehmen — der Konfigblock kommt aus der Datei,
376
+ nicht aus dem Katalog. Ist die Datei mit dem Team geteilt, verlieren die anderen den Server, sobald
377
+ die Änderung committet ist. Nur claude-code hat eine solche Datei und nur an einem
378
+ Arbeitsverzeichnis. Gearbeitet wird in zwei Schritten, erst eintragen, dann entfernen: bricht der
379
+ zweite ab, steht der Eintrag doppelt statt gar nicht mehr, und die Meldung sagt, was zu tun bleibt.
380
+
381
+ Nach dem Einhängen/Aushängen nennt hmgr den Weg zur Wirkung: Claude Code übernimmt die Änderung erst
382
+ mit der nächsten Sitzung, Hermes lädt seine config.yaml selbst nach, dafür gibt es keinen Hinweis.
383
+
384
+ Fehlende \${VAR}-Verweise beim Einhängen sind kein Abbruch, nur eine Warnung (F39): hmgr kann nicht
385
+ wissen, was die Shell beim Start des Harness setzt. Geprüft wird gegen die eigene Prozess-Umgebung
386
+ und, bei einem Hermes-Ziel, zusätzlich gegen <hermes-home>/.env.
387
+
388
+ Schalter (add, remove und migrate):
389
+ --harness <name> claude-code oder hermes, Standard claude-code
390
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home
391
+
392
+ Beispiele:
393
+ hmgr mcp list
394
+ hmgr mcp add teamwork --ort dir:.
395
+ hmgr mcp add context7 --harness hermes --ort profil:researcher
396
+ hmgr mcp remove teamwork --ort dir:.
397
+ hmgr mcp migrate kollegen-tool --ort dir:.
398
+
399
+ Häufige Fälle:
400
+ „steht nicht im Katalog": hmgr mcp list zeigt die bekannten Namen, ~/.hmgr/mcp.yaml pflegt sie.
401
+ „kennt keine MCP-Server": nur claude-code und hermes haben diese Fähigkeit, agents nicht.
402
+ „kennt für MCP-Server den Ort … nicht – Hermes kennt keine Projektebene für MCPs": bei Hermes
403
+ --ort home oder --ort profil:<name> nutzen, kein dir:<pfad>.
404
+ „liegt schon ein MCP-Server …, der nicht von hmgr stammt oder vom Katalogblock abweicht": von Hand
405
+ angleichen oder hmgr mcp remove, dann erneut versuchen.
406
+ „steht in der geteilten …/.mcp.json": mcp remove fasst sie nicht an, hmgr mcp migrate <name> holt
407
+ den Eintrag in den privaten Scope und löscht ihn aus der Datei.
408
+ „gibt es keine geteilte Konfigurationsdatei": mcp migrate geht nur mit --harness claude-code und
409
+ --ort dir:<pfad>.
410
+
411
+ Siehe auch: hmgr help harness, hmgr help status`;
229
412
  const SYNC = `hmgr sync [pfad]
230
413
 
231
414
  Bringt ein Arbeitsverzeichnis auf den Stand seines Zeigers: fehlende Quellen übernehmen, fehlende
@@ -256,6 +439,18 @@ Plugin-Fähigkeit deren Plugins nach (E14). Die Links zeigen weiter auf dieselbe
256
439
  nicht erneuert werden. Ein Checkout mit lokaler Änderung wird übersprungen und gemeldet (E15),
257
440
  damit nichts verloren geht. Lokale Quellen haben keinen Checkout und bleiben unberührt.
258
441
 
442
+ Unter jeder „aktualisiert"-Zeile steht, welche Skills der neue Stand geändert hat (E70): die Namen
443
+ kommen aus dem Vergleich von altem und neuem Commit, gezählt werden nur Skills, die in diesem
444
+ Checkout auch ausgecheckt sind. Berührt ein Commit nur Dateien daneben (README, CI), bleibt die
445
+ Zeile weg.
446
+
447
+ Im selben Lauf gleicht update die installierten Bundles gegen ihre Definitionen ab (E69): eine
448
+ Definition, die inzwischen ein Mitglied mehr oder weniger nennt, wird in jedem Ziel nachgezogen, in
449
+ dem das Bundle hängt. Das ist der Weg, auf dem eine Bundle-Änderung aus einer Quelle ankommt — ohne
450
+ ihn müsste man von seinen eigenen Änderungen wissen, um sie zu holen. Was sich nicht vollständig
451
+ nachziehen lässt, bleibt in seinem Ziel auf dem alten Stand und wird als fehlgeschlagen gemeldet,
452
+ ohne den übrigen Lauf aufzuhalten. hmgr bundle sync macht dasselbe für ein einzelnes Bundle.
453
+
259
454
  Danach räumt update verwaiste Ref-Verzeichnisse auf: Checkouts, in die kein Link mehr zeigt, weil
260
455
  ein Link inzwischen auf einen anderen Ref umgelegt wurde. Vor dem Löschen fragt hmgr einmal für
261
456
  alle Kandidaten. Ohne Bestätigung, also auch ohne Terminal, bleibt alles liegen und wird nur
@@ -359,6 +554,8 @@ const THEMEN = new Map([
359
554
  ['unlink', UNLINK],
360
555
  ['adopt', ADOPT],
361
556
  ['plugin', PLUGIN],
557
+ ['bundle', BUNDLE],
558
+ ['mcp', MCP],
362
559
  ['sync', SYNC],
363
560
  ['update', UPDATE],
364
561
  ['status', STATUS],
package/dist/cli/main.js CHANGED
@@ -4,16 +4,19 @@ import path from 'node:path';
4
4
  import { realpathSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
- import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
8
- import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
7
+ import { Anwendungsfaelle, } from "../kern/anwendungsfaelle.js";
8
+ import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, 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";
16
+ import { paketVersion } from "../adapter/paket-version.js";
15
17
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
16
18
  import { TerminalRueckfrage } from "../adapter/terminal.js";
19
+ import { ProzessUmgebung } from "../adapter/umgebung.js";
17
20
  import { ZustandJson } from "../adapter/zustand-json.js";
18
21
  import { HILFE, hilfeText } from "./hilfe.js";
19
22
  export function umgebungLesen(env = process.env) {
@@ -38,6 +41,8 @@ export function bauen(umgebung, optionen = {}) {
38
41
  zeiger: new ComposerZeiger(),
39
42
  zustand,
40
43
  rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
44
+ mcpKatalog: new DateiMcpKatalog(path.join(umgebung.hmgrHome, 'mcp.yaml')),
45
+ umgebung: optionen.umgebung ?? new ProzessUmgebung(),
41
46
  };
42
47
  return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
43
48
  }
@@ -55,6 +60,8 @@ const SCHALTER = {
55
60
  unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
56
61
  adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
57
62
  plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
63
+ bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: [] },
64
+ mcp: { werte: ['harness', 'ort'], flaggen: [] },
58
65
  sync: { werte: [], flaggen: [] },
59
66
  update: { werte: [], flaggen: [] },
60
67
  status: { werte: [], flaggen: [] },
@@ -82,6 +89,20 @@ const UNTERKOMMANDOS = {
82
89
  remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
83
90
  list: { woerter: 1, schalter: [] },
84
91
  },
92
+ bundle: {
93
+ add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle'] },
94
+ remove: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
95
+ // sync nimmt einen Bundle-Namen, aber keinen zwingend: ohne Namen gilt der ganze Bestand.
96
+ sync: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
97
+ list: { woerter: 1, schalter: [] },
98
+ },
99
+ mcp: {
100
+ add: { woerter: 2, schalter: ['harness', 'ort'] },
101
+ remove: { woerter: 2, schalter: ['harness', 'ort'] },
102
+ // Übernehmen (E64): dieselben Ziel-Schalter wie add und remove.
103
+ migrate: { woerter: 2, schalter: ['harness', 'ort'] },
104
+ list: { woerter: 1, schalter: [] },
105
+ },
85
106
  harness: {
86
107
  list: { woerter: 1, schalter: [] },
87
108
  enable: { woerter: 2, schalter: [] },
@@ -167,6 +188,11 @@ const UEBERNAHME_GRUND = {
167
188
  export async function ausfuehren(argv, umgebung, optionen = {}) {
168
189
  const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
169
190
  const kommando = argv[0];
191
+ // Version vor allem anderen (E72): nur die Nummer, damit ein Skript sie ohne Nacharbeit liest.
192
+ if (kommando === '--version') {
193
+ console.log(paketVersion());
194
+ return 0;
195
+ }
170
196
  // Hilfe zuerst: --help ist überall eine Flagge, damit kein Kommando sie als Wert liest.
171
197
  if (kommando === undefined) {
172
198
  console.log(HILFE);
@@ -207,7 +233,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
207
233
  console.log(quelleAlsText(quelle) === quelleAlsText(genannt)
208
234
  ? 'Quelle ist schon bekannt, keine zweite angelegt.'
209
235
  : `Quelle ist schon bekannt, geführt als ${quelleAlsText(quelle)}. Keine zweite angelegt.`);
210
- console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}) als „${quelleName(quelle)}", ${skills.length} Skills:`);
236
+ console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}) als „${quelleName(quelle)}", ${skills.length} ${skills.length === 1 ? 'Skill' : 'Skills'}:`);
211
237
  for (const skill of skills) {
212
238
  const beschreibung = skill.beschreibung === undefined ? '' : ` ${kappen(skill.beschreibung)}`;
213
239
  console.log(` ${skill.name} ${skill.pfad}${beschreibung}`);
@@ -365,6 +391,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
365
391
  if (kommando === 'unlink') {
366
392
  const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
367
393
  console.log(`Link ${skill} entfernt.`);
394
+ // Zerbrochen (E69): die übrigen Links des Bundles bleiben als Einzel-Installationen liegen.
395
+ for (const bundle of ergebnis.zerbrochen)
396
+ console.log(`Bundle „${bundle}" ist damit zerbrochen, seine übrigen Links bleiben als einzelne Links liegen.`);
368
397
  if (ergebnis.hinweis !== undefined)
369
398
  console.log(ergebnis.hinweis);
370
399
  return 0;
@@ -478,6 +507,147 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
478
507
  console.log(` ${zeile}`);
479
508
  return 0;
480
509
  }
510
+ if (kommando === 'bundle') {
511
+ const schalter = schalterLesen(argv.slice(1), 'bundle');
512
+ unterkommandoPruefen('bundle', schalter);
513
+ const { rest, werte } = schalter;
514
+ const unterkommando = rest[0];
515
+ if (unterkommando === 'list') {
516
+ const katalog = await anwendungsfaelle.bundleKatalog();
517
+ const leer = katalog.every((k) => k.bundles.length === 0 && k.kaputte.length === 0 && k.fehler === undefined);
518
+ if (leer)
519
+ console.log('Keine Quelle mit Bundles. hmgr help bundle zeigt das Format von bundles/<name>.yaml.');
520
+ for (const eintrag of katalog) {
521
+ if (eintrag.fehler !== undefined) {
522
+ console.log(`Quelle ${quelleName(eintrag.quelle)} (${quelleAlsText(eintrag.quelle)}) nicht lesbar: ${eintrag.fehler}`);
523
+ continue;
524
+ }
525
+ if (eintrag.bundles.length === 0 && eintrag.kaputte.length === 0)
526
+ continue;
527
+ console.log(`Quelle ${quelleName(eintrag.quelle)} (${quelleAlsText(eintrag.quelle)})`);
528
+ for (const bundle of eintrag.bundles) {
529
+ console.log(` ${bundle.name} ${kappen(bundle.beschreibung)}`);
530
+ for (const mitglied of bundle.mitglieder)
531
+ console.log(` ${mitglied.pfad} aus ${quelleAlsText(mitglied.quelle)}`);
532
+ }
533
+ // Eine kaputte Definition verdeckt die übrigen nicht, sie wird eigens genannt (E69).
534
+ for (const kaputt of eintrag.kaputte)
535
+ console.log(` Fehler ${kaputt.datei}: ${kaputt.fehler}`);
536
+ }
537
+ return 0;
538
+ }
539
+ if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'sync')
540
+ throw new Error(`${unterkommando === undefined ? 'bundle braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für bundle`}: bundle add <name>, bundle remove <name>, bundle sync [name] oder bundle list. hmgr help bundle zeigt Beispiele.`);
541
+ const name = rest[1];
542
+ if (name === undefined && unterkommando !== 'sync')
543
+ throw new Error(`bundle ${unterkommando} braucht einen Bundle-Namen. hmgr bundle list zeigt die Bundles der bekannten Quellen.`);
544
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
545
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
546
+ // Wie bei link: Verzeichnis merken und binden, damit status es wiederfindet (E21).
547
+ const composer = werte.get('composer');
548
+ if (composer === undefined)
549
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
550
+ else
551
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
552
+ }
553
+ if (unterkommando === 'add') {
554
+ const quelle = werte.get('quelle');
555
+ const ergebnis = await anwendungsfaelle.bundleInstallieren({
556
+ bundle: name,
557
+ ziel,
558
+ ...(quelle === undefined ? {} : { quelle }),
559
+ });
560
+ console.log(`Bundle ${ergebnis.bundle.name} in ${ziel.harness} ${beschreibeOrt(ziel.ort)} installiert (${quelleAlsText(ergebnis.bundle.quelle)}).`);
561
+ for (const neu of ergebnis.neueQuellen)
562
+ console.log(` neue Quelle ${quelleAlsText(neu)} als „${quelleName(neu)}"`);
563
+ if (ergebnis.gelegt.length > 0)
564
+ console.log(` gelegt ${ergebnis.gelegt.join(', ')}`);
565
+ // Übernommen (F67): schon vorhandene Links derselben Quelle gehören jetzt zum Bundle.
566
+ if (ergebnis.uebernommen.length > 0)
567
+ console.log(` übernommen ${ergebnis.uebernommen.join(', ')}`);
568
+ if (ergebnis.hinweis !== undefined)
569
+ for (const zeile of ergebnis.hinweis.split('\n'))
570
+ console.log(` ${zeile}`);
571
+ return 0;
572
+ }
573
+ if (unterkommando === 'remove') {
574
+ const ergebnis = await anwendungsfaelle.bundleEntfernen({ bundle: name, ziel });
575
+ console.log(`Bundle ${ergebnis.bundle} aus ${ziel.harness} ${beschreibeOrt(ziel.ort)} entfernt.`);
576
+ console.log(ergebnis.entfernt.length === 0
577
+ ? ' Kein Link mehr da, nur der Bundle-Verweis ist weg.'
578
+ : ` entfernt ${ergebnis.entfernt.join(', ')}`);
579
+ return 0;
580
+ }
581
+ // sync: ohne Ziel-Schalter der ganze Bestand, mit --ort/--harness nur dieses Ziel.
582
+ const nurZiel = werte.has('harness') || werte.has('ort');
583
+ const eintraege = await anwendungsfaelle.bundleAbgleich({
584
+ ...(name === undefined ? {} : { bundle: name }),
585
+ ...(nurZiel ? { ziel } : {}),
586
+ });
587
+ if (eintraege.length === 0)
588
+ console.log('Kein Bundle installiert, nichts abzugleichen.');
589
+ for (const zeile of bundleBericht(eintraege))
590
+ console.log(zeile);
591
+ return 0;
592
+ }
593
+ if (kommando === 'mcp') {
594
+ const schalter = schalterLesen(argv.slice(1), 'mcp');
595
+ unterkommandoPruefen('mcp', schalter);
596
+ const { rest, werte } = schalter;
597
+ const unterkommando = rest[0];
598
+ if (unterkommando === 'list') {
599
+ const uebersicht = await anwendungsfaelle.mcpUebersicht();
600
+ if (uebersicht.katalog.length === 0)
601
+ console.log('Kein MCP im Katalog. hmgr help mcp zeigt das Format von ~/.hmgr/mcp.yaml.');
602
+ for (const { eintrag, eingehaengt, geteilteFundorte } of uebersicht.katalog) {
603
+ console.log(`${eintrag.name} ${eintrag.beschreibung}`);
604
+ if (eingehaengt.length === 0 && geteilteFundorte.length === 0)
605
+ console.log(' nirgends eingehängt');
606
+ for (const ziel of eingehaengt)
607
+ console.log(` ${ziel.harness} ${beschreibeOrt(ziel.ort)}`);
608
+ // Geteilte, nur lesbare Fundorte (E61-Nachtrag): eigens gekennzeichnet, hmgr fasst sie nicht an.
609
+ for (const f of geteilteFundorte)
610
+ console.log(` ${f.ziel.harness} ${beschreibeOrt(f.ziel.ort)} ${f.datei} · Scope ${f.scope} (geteilt, hmgr fasst sie nicht an)`);
611
+ }
612
+ if (uebersicht.unbekannt.length > 0 || uebersicht.unbekannteGeteilteFundorte.length > 0) {
613
+ console.log('Nicht im Katalog, aber eingehängt (hmgr fasst sie nicht an):');
614
+ for (const { ziel, name } of uebersicht.unbekannt)
615
+ console.log(` ${name} ${ziel.harness} ${beschreibeOrt(ziel.ort)}`);
616
+ for (const f of uebersicht.unbekannteGeteilteFundorte)
617
+ console.log(` ${f.name} ${f.ziel.harness} ${beschreibeOrt(f.ziel.ort)} ${f.datei} · Scope ${f.scope}`);
618
+ }
619
+ return 0;
620
+ }
621
+ if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'migrate')
622
+ 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.`);
623
+ const name = rest[1];
624
+ if (name === undefined)
625
+ throw new Error(`mcp ${unterkommando} braucht einen MCP-Namen. hmgr mcp list zeigt die Katalog-Einträge.`);
626
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
627
+ // Übernehmen (E64): der Name muss nicht im Katalog stehen, der Block kommt aus der geteilten Datei.
628
+ if (unterkommando === 'migrate') {
629
+ const ergebnis = await anwendungsfaelle.mcpUebernehmen({ name, ziel });
630
+ console.log(`MCP ${ergebnis.name} in den privaten Scope von ${ziel.harness} ${beschreibeOrt(ziel.ort)} übernommen `
631
+ + `und aus ${ergebnis.datei} entfernt.`);
632
+ if (ergebnis.hinweis !== undefined)
633
+ console.log(` ${ergebnis.hinweis}`);
634
+ return 0;
635
+ }
636
+ if (unterkommando === 'add') {
637
+ const ergebnis = await anwendungsfaelle.mcpEinhaengen({ name, ziel });
638
+ console.log(`MCP ${ergebnis.eintrag.name} eingehängt in ${ziel.harness} ${beschreibeOrt(ziel.ort)}.`);
639
+ if (ergebnis.hinweis !== undefined)
640
+ console.log(` ${ergebnis.hinweis}`);
641
+ if (ergebnis.warnung !== undefined)
642
+ console.log(` ${ergebnis.warnung}`);
643
+ return 0;
644
+ }
645
+ const ergebnis = await anwendungsfaelle.mcpAushaengen({ name, ziel });
646
+ console.log(`MCP ${ergebnis.eintrag.name} ausgehängt aus ${ziel.harness} ${beschreibeOrt(ziel.ort)}.`);
647
+ if (ergebnis.hinweis !== undefined)
648
+ console.log(` ${ergebnis.hinweis}`);
649
+ return 0;
650
+ }
481
651
  if (kommando === 'sync') {
482
652
  const { rest } = schalterLesen(argv.slice(1), 'sync');
483
653
  restPruefen(rest, 1, 'sync');
@@ -498,13 +668,18 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
498
668
  }
499
669
  if (kommando === 'update') {
500
670
  restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
501
- const { checkouts: liste, pluginHarnesses, aufraeumen } = await anwendungsfaelle.aktualisieren();
671
+ const { checkouts: liste, pluginHarnesses, bundles, aufraeumen } = await anwendungsfaelle.aktualisieren();
502
672
  if (liste.length === 0)
503
673
  console.log('Keine Checkouts.');
504
- for (const { checkout, aktualisierung } of liste) {
674
+ for (const { checkout, aktualisierung, geaenderteSkills } of liste) {
505
675
  const kopf = `${quelleAlsText(checkout.quelle)} @ ${checkout.ref}`;
506
- if (aktualisierung.ergebnis === 'aktualisiert')
676
+ if (aktualisierung.ergebnis === 'aktualisiert') {
507
677
  console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
678
+ // Auf Skill-Ebene berichten (E70): sonst steht da nur, dass die Quelle neue Commits hat.
679
+ // Ohne Treffer bleibt die Zeile weg — der Commit hat dann nichts in einem Skill geändert.
680
+ if (geaenderteSkills.length > 0)
681
+ console.log(` ${zahlwort(geaenderteSkills.length, 'Skill', 'Skills')} geändert: ${geaenderteSkills.join(', ')}`);
682
+ }
508
683
  else if (aktualisierung.ergebnis === 'unveraendert')
509
684
  console.log(` unverändert ${kopf}`);
510
685
  else if (aktualisierung.ergebnis === 'uebersprungen')
@@ -514,6 +689,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
514
689
  }
515
690
  for (const name of pluginHarnesses)
516
691
  console.log(` Plugins ${name} aktualisiert (E14)`);
692
+ // Bundles im selben Lauf (E69, F64=O1): eine geänderte Definition wird hier nachgezogen.
693
+ for (const zeile of bundleBericht(bundles))
694
+ console.log(zeile);
517
695
  // Zum Schluss die verwaisten Checkouts: was gelöscht wurde und was liegen bleibt.
518
696
  for (const verwaist of aufraeumen.verwaiste) {
519
697
  const kopf = `${quelleAlsText(verwaist.checkout.quelle)} @ ${verwaist.checkout.ref}`;
@@ -548,6 +726,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
548
726
  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))}`);
549
727
  }
550
728
  }
729
+ // Bundles des Ziels (E69), unter den Einträgen: welche Links als Einheit zusammengehören.
730
+ for (const bundle of zielZustand.bundles)
731
+ console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundle.skills.join(', ')}`);
551
732
  if (zielZustand.plugins !== undefined)
552
733
  console.log(` Plugins: ${zielZustand.plugins.length === 0 ? 'keine' : zielZustand.plugins.join(', ')}`);
553
734
  }
@@ -668,6 +849,31 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
668
849
  }
669
850
  throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
670
851
  }
852
+ /**
853
+ * Bericht über den Bundle-Abgleich (E69), gleich für `hmgr bundle sync` und `hmgr update`: je
854
+ * Bundle eine Zeile mit seinem Ausgang, darunter, was sich geändert hat. „fehlgeschlagen" heißt
855
+ * genau das — der Zusatz sagt, dass das Bundle im Ziel auf dem alten Stand bleibt.
856
+ */
857
+ function bundleBericht(eintraege) {
858
+ const zeilen = [];
859
+ for (const eintrag of eintraege) {
860
+ const kopf = `Bundle ${eintrag.bundle} ${eintrag.ziel.harness} ${beschreibeOrt(eintrag.ziel.ort)}`;
861
+ if (eintrag.ergebnis === 'fehlgeschlagen') {
862
+ zeilen.push(` fehlgeschlagen ${kopf} bleibt auf dem alten Stand`);
863
+ for (const zeile of (eintrag.fehler ?? '').split('\n'))
864
+ zeilen.push(` ${zeile}`);
865
+ continue;
866
+ }
867
+ zeilen.push(` ${eintrag.ergebnis === 'aktualisiert' ? 'aktualisiert ' : 'unverändert '} ${kopf}`);
868
+ for (const neu of eintrag.neueQuellen)
869
+ zeilen.push(` neue Quelle ${quelleAlsText(neu)}`);
870
+ if (eintrag.gelegt.length > 0)
871
+ zeilen.push(` gelegt ${eintrag.gelegt.join(', ')}`);
872
+ if (eintrag.entfernt.length > 0)
873
+ zeilen.push(` entfernt ${eintrag.entfernt.join(', ')}`);
874
+ }
875
+ return zeilen;
876
+ }
671
877
  /** Grund eines Links als Suffix (E37). „manuell" bleibt stumm, damit die Ausgabe ruhig bleibt. */
672
878
  function beschreibeGrund(grund) {
673
879
  if (grund === undefined || grund.art === 'manuell')
@@ -691,6 +897,7 @@ function beschreibeOrt(ort) {
691
897
  }
692
898
  function istModellFehler(fehler) {
693
899
  return (fehler instanceof NameBelegt ||
900
+ fehler instanceof LinkAnlegenFehlgeschlagen ||
694
901
  fehler instanceof SkillMehrdeutig ||
695
902
  fehler instanceof SkillUnbekannt ||
696
903
  fehler instanceof OrtNichtUnterstuetzt ||
@@ -703,13 +910,28 @@ function istModellFehler(fehler) {
703
910
  fehler instanceof PluginMehrdeutig ||
704
911
  fehler instanceof PluginUnbekannt ||
705
912
  fehler instanceof AbhaengigkeitFehlt ||
913
+ fehler instanceof BundleUnbekannt ||
914
+ fehler instanceof BundleMehrdeutig ||
915
+ fehler instanceof BundleMitgliederFehlen ||
916
+ fehler instanceof BundleNamenskonflikte ||
917
+ fehler instanceof BundleNichtInstalliert ||
918
+ fehler instanceof BundleDateiFehler ||
706
919
  fehler instanceof QuelleUnbekannt ||
707
920
  fehler instanceof QuelleNameBelegt ||
708
921
  fehler instanceof QuelleAufraeumenLokaleAenderungen ||
709
922
  fehler instanceof SkillOrdnerFaehigkeitFehlt ||
710
923
  fehler instanceof SkillOrdnerBereitsVerwaltet ||
711
924
  fehler instanceof SkillOrdnerNichtVerwaltet ||
712
- fehler instanceof SkillOrdnerUngueltig);
925
+ fehler instanceof SkillOrdnerUngueltig ||
926
+ fehler instanceof McpUnbekannt ||
927
+ fehler instanceof McpFaehigkeitFehlt ||
928
+ fehler instanceof McpEbeneNichtUnterstuetzt ||
929
+ fehler instanceof McpAbweichenderEintrag ||
930
+ fehler instanceof McpKatalogFehler ||
931
+ fehler instanceof McpNurGeteilterFundort ||
932
+ fehler instanceof McpUebernahmeNichtMoeglich ||
933
+ fehler instanceof McpNichtInGeteilterDatei ||
934
+ fehler instanceof McpUebernahmeUnvollstaendig);
713
935
  }
714
936
  /**
715
937
  * Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
@@ -728,6 +950,18 @@ function naechsterSchritt(fehler) {
728
950
  return 'hmgr plugin list zeigt die Plugins der bekannten Quellen. Fehlt der Marketplace, erst hmgr source add <url>.';
729
951
  if (fehler instanceof AbhaengigkeitFehlt)
730
952
  return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
953
+ if (fehler instanceof BundleUnbekannt)
954
+ return 'hmgr bundle list zeigt die Bundles der bekannten Quellen. Fehlt die Quelle, erst hmgr source add <url|pfad>.';
955
+ if (fehler instanceof BundleMehrdeutig)
956
+ return 'Quelle eingrenzen: --quelle <teilstring der URL oder des Pfads>. hmgr bundle list zeigt, wo welches Bundle steht.';
957
+ if (fehler instanceof BundleMitgliederFehlen)
958
+ return 'Das Bundle ist nur ganz oder gar nicht zu haben. Wer nur einen Teil braucht, verlinkt die erreichbaren Skills einzeln mit hmgr link.';
959
+ if (fehler instanceof BundleNamenskonflikte)
960
+ return 'Die genannten Namen erst freimachen (hmgr unlink <skill> bzw. hmgr adopt <name>), dann bundle add erneut.';
961
+ if (fehler instanceof BundleNichtInstalliert)
962
+ return 'hmgr status zeigt je Ziel, welche Bundles dort installiert sind.';
963
+ if (fehler instanceof BundleDateiFehler)
964
+ return 'Die Definition liegt in der Quelle und wird dort korrigiert, hmgr schreibt sie nie. hmgr help bundle zeigt das Format.';
731
965
  if (fehler instanceof KeinPassenderRef)
732
966
  return 'Die Tags der Quelle sind frisch geholt, keiner passt. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
733
967
  if (fehler instanceof HarnessNichtVorhanden)
@@ -752,6 +986,20 @@ function naechsterSchritt(fehler) {
752
986
  return fehler.grund === 'ausserhalb'
753
987
  ? 'Der Pfad muss unterhalb des Hermes-Home liegen.'
754
988
  : 'Das ist Hermes\' eigener Skill-Ordner, ein anderer Pfad muss her.';
989
+ if (fehler instanceof McpUnbekannt)
990
+ return 'hmgr mcp list zeigt die Katalog-Einträge. Gepflegt wird der Katalog in ~/.hmgr/mcp.yaml, hmgr help mcp zeigt das Format.';
991
+ if (fehler instanceof McpFaehigkeitFehlt)
992
+ return 'Nur claude-code und hermes kennen MCP-Server.';
993
+ if (fehler instanceof McpEbeneNichtUnterstuetzt)
994
+ return 'claude-code kennt --ort home und --ort dir:<pfad>, hermes --ort home und --ort profil:<name>.';
995
+ if (fehler instanceof McpAbweichenderEintrag)
996
+ return 'Erst von Hand angleichen oder mit hmgr mcp remove entfernen, dann erneut versuchen.';
997
+ if (fehler instanceof McpKatalogFehler)
998
+ return '~/.hmgr/mcp.yaml prüfen und korrigieren, hmgr help mcp zeigt das Format.';
999
+ if (fehler instanceof McpUebernahmeNichtMoeglich)
1000
+ return 'Eine geteilte .mcp.json kennt nur claude-code, und nur an einem Arbeitsverzeichnis: --harness claude-code --ort dir:<pfad>.';
1001
+ if (fehler instanceof McpNichtInGeteilterDatei)
1002
+ return 'hmgr mcp list zeigt, welche Namen dort stehen; mcp migrate übernimmt nur Einträge aus der geteilten Datei.';
755
1003
  return undefined;
756
1004
  }
757
1005
  export async function main(argv, umgebung) {