hmgr 0.0.6 → 0.0.12

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/main.js CHANGED
@@ -4,19 +4,24 @@ 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, 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";
7
+ import { Anwendungsfaelle, } from "../kern/anwendungsfaelle.js";
8
+ import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimOhneCliZugriff, HeimUnbekannt, 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, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
9
9
  import { Agents } from "../adapter/agents.js";
10
- import { ClaudeCode } from "../adapter/claude-code.js";
10
+ import { ClaudeCode, claudeAusfuehrer } from "../adapter/claude-code.js";
11
11
  import { Hermes } from "../adapter/hermes.js";
12
12
  import { ComposerZeiger } from "../adapter/composer.js";
13
- import { DateisystemZiele } from "../adapter/dateisystem.js";
13
+ import { DateisystemZiele, LokalerZugriff } from "../adapter/dateisystem.js";
14
+ import { ZustandHeime } from "../adapter/heime.js";
15
+ import { WslBruecke, WslZugriff, wslAusfuehrer, wslDistros } from "../adapter/wsl.js";
14
16
  import { GitQuellen } from "../adapter/git.js";
15
17
  import { DateiMcpKatalog } from "../adapter/mcp-katalog.js";
18
+ import { cliUpdateHinweis } from "../adapter/npm-registry.js";
19
+ import { paketVersion } from "../adapter/paket-version.js";
16
20
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
17
21
  import { TerminalRueckfrage } from "../adapter/terminal.js";
18
22
  import { ProzessUmgebung } from "../adapter/umgebung.js";
19
23
  import { ZustandJson } from "../adapter/zustand-json.js";
24
+ import { VerlaufJson } from "../adapter/verlauf-json.js";
20
25
  import { HILFE, hilfeText } from "./hilfe.js";
21
26
  export function umgebungLesen(env = process.env) {
22
27
  const home = env.HOME ?? homedir();
@@ -29,19 +34,45 @@ export function umgebungLesen(env = process.env) {
29
34
  }
30
35
  export function bauen(umgebung, optionen = {}) {
31
36
  const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'));
37
+ const verlauf = new VerlaufJson(umgebung.hmgrHome);
32
38
  const wurzel = path.join(umgebung.hmgrHome, 'checkouts');
33
- const claude = optionen.claudeAusfuehrer === undefined
34
- ? new ClaudeCode(umgebung.home)
35
- : new ClaudeCode(umgebung.home, optionen.claudeAusfuehrer);
39
+ const umgebungPort = optionen.umgebung ?? new ProzessUmgebung();
40
+ // Homedirs aus dem Zustand (E78): das primäre lässt sich umstellen (F89), dazu kommen
41
+ // zusätzliche in einer WSL-Distro (F87). `wsl:`-Heime fallen außerhalb von Windows heraus.
42
+ const heime = new ZustandHeime(zustand, umgebung.home, umgebungPort.plattform());
43
+ const wslAusfuehren = optionen.wslAusfuehrer ?? wslAusfuehrer;
44
+ // Je Distro eine Bridge, damit die einmal ermittelte Automount-Wurzel im Lauf erhalten bleibt.
45
+ const bruecken = new Map();
46
+ const bruecke = (distro) => {
47
+ let vorhanden = bruecken.get(distro);
48
+ if (vorhanden === undefined) {
49
+ vorhanden = new WslBruecke(distro, wslAusfuehren);
50
+ bruecken.set(distro, vorhanden);
51
+ }
52
+ return vorhanden;
53
+ };
54
+ const lokal = new LokalerZugriff();
55
+ const zugriffFuerOrt = (ort) => {
56
+ const adresse = ort.art === 'home' ? ort.heim : undefined;
57
+ const zerlegt = adresse === undefined ? undefined : heimAdresseZerlegen(adresse);
58
+ return zerlegt === undefined ? lokal : new WslZugriff(bruecke(zerlegt.distro));
59
+ };
60
+ const claude = new ClaudeCode(heime, optionen.claudeAusfuehrer ?? claudeAusfuehrer, zugriffFuerOrt);
36
61
  const ports = {
37
- harnesses: [claude, new Hermes(umgebung.home), new Agents(umgebung.home)],
62
+ harnesses: [claude, new Hermes(heime), new Agents(heime)],
38
63
  quellen: new GitQuellen(wurzel),
39
- ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome),
64
+ ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome, (ziel) => zugriffFuerOrt(ziel.ort)),
40
65
  zeiger: new ComposerZeiger(),
41
66
  zustand,
42
67
  rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
43
68
  mcpKatalog: new DateiMcpKatalog(path.join(umgebung.hmgrHome, 'mcp.yaml')),
44
- umgebung: optionen.umgebung ?? new ProzessUmgebung(),
69
+ umgebung: umgebungPort,
70
+ heime,
71
+ wsl: {
72
+ distros: async () => await wslDistros(wslAusfuehren),
73
+ home: async (distro) => await bruecke(distro).home(),
74
+ },
75
+ verlauf,
45
76
  };
46
77
  return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
47
78
  }
@@ -59,10 +90,12 @@ const SCHALTER = {
59
90
  unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
60
91
  adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
61
92
  plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
93
+ bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: [] },
62
94
  mcp: { werte: ['harness', 'ort'], flaggen: [] },
63
95
  sync: { werte: [], flaggen: [] },
64
96
  update: { werte: [], flaggen: [] },
65
97
  status: { werte: [], flaggen: [] },
98
+ log: { werte: [], flaggen: [] },
66
99
  harness: { werte: ['profil'], flaggen: [] },
67
100
  };
68
101
  /**
@@ -87,6 +120,13 @@ const UNTERKOMMANDOS = {
87
120
  remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
88
121
  list: { woerter: 1, schalter: [] },
89
122
  },
123
+ bundle: {
124
+ add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle'] },
125
+ remove: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
126
+ // sync nimmt einen Bundle-Namen, aber keinen zwingend: ohne Namen gilt der ganze Bestand.
127
+ sync: { woerter: 2, schalter: ['harness', 'ort', 'composer'] },
128
+ list: { woerter: 1, schalter: [] },
129
+ },
90
130
  mcp: {
91
131
  add: { woerter: 2, schalter: ['harness', 'ort'] },
92
132
  remove: { woerter: 2, schalter: ['harness', 'ort'] },
@@ -156,6 +196,11 @@ function unterkommandoPruefen(kommando, schalter) {
156
196
  `hmgr help ${kommando} zeigt die Schalter.`);
157
197
  restPruefen(schalter.rest, menge.woerter, kommando, unterkommando);
158
198
  }
199
+ /**
200
+ * `--ort` deuten. Ein zusätzliches Homedir (E78, F87) heißt nach seiner Adresse: `--ort wsl:Ubuntu`,
201
+ * auch in der ausgeschriebenen Form `--ort home:wsl:Ubuntu`. Beides ergibt denselben Ort, damit
202
+ * niemand raten muss, ob das `home:` davor gehört.
203
+ */
159
204
  function ortLesen(text) {
160
205
  const roh = text ?? 'home';
161
206
  if (roh === 'home')
@@ -164,7 +209,10 @@ function ortLesen(text) {
164
209
  return { art: 'arbeitsverzeichnis', pfad: path.resolve(roh.slice(4)) };
165
210
  if (roh.startsWith('profil:'))
166
211
  return { art: 'profil', name: roh.slice(7) };
167
- throw new Error(`Unbekannter Ort „${roh}", erwartet home, dir:<pfad> oder profil:<name>`);
212
+ const heim = heimAdresseZerlegen(roh.startsWith('home:') ? roh.slice(5) : roh);
213
+ if (heim !== undefined)
214
+ return { art: 'home', heim: heimAdresseAlsText(heim) };
215
+ throw new Error(`Unbekannter Ort „${roh}", erwartet home, wsl:<distro>, dir:<pfad> oder profil:<name>`);
168
216
  }
169
217
  /** Klartext für den Grund, aus dem ein fremder Eintrag liegen bleibt (B12). */
170
218
  const UEBERNAHME_GRUND = {
@@ -179,6 +227,11 @@ const UEBERNAHME_GRUND = {
179
227
  export async function ausfuehren(argv, umgebung, optionen = {}) {
180
228
  const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
181
229
  const kommando = argv[0];
230
+ // Version vor allem anderen (E72): nur die Nummer, damit ein Skript sie ohne Nacharbeit liest.
231
+ if (kommando === '--version') {
232
+ console.log(paketVersion());
233
+ return 0;
234
+ }
182
235
  // Hilfe zuerst: --help ist überall eine Flagge, damit kein Kommando sie als Wert liest.
183
236
  if (kommando === undefined) {
184
237
  console.log(HILFE);
@@ -203,6 +256,14 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
203
256
  console.log(text);
204
257
  return 0;
205
258
  }
259
+ // Versionssprung vermerken (E80): bei jedem echten Kommando, Hilfe und Version sind oben schon
260
+ // zurückgekehrt. Ein Fehler hier (etwa ein kaputtes verlauf.json) darf kein Kommando kippen.
261
+ try {
262
+ await anwendungsfaelle.versionsSprungVermerken('cli', paketVersion(), 'cli');
263
+ }
264
+ catch {
265
+ // still, siehe oben.
266
+ }
206
267
  if (kommando === 'source') {
207
268
  const schalter = schalterLesen(argv.slice(1), 'source');
208
269
  unterkommandoPruefen('source', schalter);
@@ -377,6 +438,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
377
438
  if (kommando === 'unlink') {
378
439
  const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
379
440
  console.log(`Link ${skill} entfernt.`);
441
+ // Zerbrochen (E69): die übrigen Links des Bundles bleiben als Einzel-Installationen liegen.
442
+ for (const bundle of ergebnis.zerbrochen)
443
+ console.log(`Bundle „${bundle}" ist damit zerbrochen, seine übrigen Links bleiben als einzelne Links liegen.`);
380
444
  if (ergebnis.hinweis !== undefined)
381
445
  console.log(ergebnis.hinweis);
382
446
  return 0;
@@ -490,6 +554,89 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
490
554
  console.log(` ${zeile}`);
491
555
  return 0;
492
556
  }
557
+ if (kommando === 'bundle') {
558
+ const schalter = schalterLesen(argv.slice(1), 'bundle');
559
+ unterkommandoPruefen('bundle', schalter);
560
+ const { rest, werte } = schalter;
561
+ const unterkommando = rest[0];
562
+ if (unterkommando === 'list') {
563
+ const katalog = await anwendungsfaelle.bundleKatalog();
564
+ const leer = katalog.every((k) => k.bundles.length === 0 && k.kaputte.length === 0 && k.fehler === undefined);
565
+ if (leer)
566
+ console.log('Keine Quelle mit Bundles. hmgr help bundle zeigt das Format von bundles/<name>.yaml.');
567
+ for (const eintrag of katalog) {
568
+ if (eintrag.fehler !== undefined) {
569
+ console.log(`Quelle ${quelleName(eintrag.quelle)} (${quelleAlsText(eintrag.quelle)}) nicht lesbar: ${eintrag.fehler}`);
570
+ continue;
571
+ }
572
+ if (eintrag.bundles.length === 0 && eintrag.kaputte.length === 0)
573
+ continue;
574
+ console.log(`Quelle ${quelleName(eintrag.quelle)} (${quelleAlsText(eintrag.quelle)})`);
575
+ for (const bundle of eintrag.bundles) {
576
+ console.log(` ${bundle.name} ${kappen(bundle.beschreibung)}`);
577
+ for (const mitglied of bundle.mitglieder)
578
+ console.log(` ${mitglied.pfad} aus ${quelleAlsText(mitglied.quelle)}`);
579
+ }
580
+ // Eine kaputte Definition verdeckt die übrigen nicht, sie wird eigens genannt (E69).
581
+ for (const kaputt of eintrag.kaputte)
582
+ console.log(` Fehler ${kaputt.datei}: ${kaputt.fehler}`);
583
+ }
584
+ return 0;
585
+ }
586
+ if (unterkommando !== 'add' && unterkommando !== 'remove' && unterkommando !== 'sync')
587
+ 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.`);
588
+ const name = rest[1];
589
+ if (name === undefined && unterkommando !== 'sync')
590
+ throw new Error(`bundle ${unterkommando} braucht einen Bundle-Namen. hmgr bundle list zeigt die Bundles der bekannten Quellen.`);
591
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
592
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
593
+ // Wie bei link: Verzeichnis merken und binden, damit status es wiederfindet (E21).
594
+ const composer = werte.get('composer');
595
+ if (composer === undefined)
596
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
597
+ else
598
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
599
+ }
600
+ if (unterkommando === 'add') {
601
+ const quelle = werte.get('quelle');
602
+ const ergebnis = await anwendungsfaelle.bundleInstallieren({
603
+ bundle: name,
604
+ ziel,
605
+ ...(quelle === undefined ? {} : { quelle }),
606
+ });
607
+ console.log(`Bundle ${ergebnis.bundle.name} in ${ziel.harness} ${beschreibeOrt(ziel.ort)} installiert (${quelleAlsText(ergebnis.bundle.quelle)}).`);
608
+ for (const neu of ergebnis.neueQuellen)
609
+ console.log(` neue Quelle ${quelleAlsText(neu)} als „${quelleName(neu)}"`);
610
+ if (ergebnis.gelegt.length > 0)
611
+ console.log(` gelegt ${ergebnis.gelegt.join(', ')}`);
612
+ // Übernommen (F67): schon vorhandene Links derselben Quelle gehören jetzt zum Bundle.
613
+ if (ergebnis.uebernommen.length > 0)
614
+ console.log(` übernommen ${ergebnis.uebernommen.join(', ')}`);
615
+ if (ergebnis.hinweis !== undefined)
616
+ for (const zeile of ergebnis.hinweis.split('\n'))
617
+ console.log(` ${zeile}`);
618
+ return 0;
619
+ }
620
+ if (unterkommando === 'remove') {
621
+ const ergebnis = await anwendungsfaelle.bundleEntfernen({ bundle: name, ziel });
622
+ console.log(`Bundle ${ergebnis.bundle} aus ${ziel.harness} ${beschreibeOrt(ziel.ort)} entfernt.`);
623
+ console.log(ergebnis.entfernt.length === 0
624
+ ? ' Kein Link mehr da, nur der Bundle-Verweis ist weg.'
625
+ : ` entfernt ${ergebnis.entfernt.join(', ')}`);
626
+ return 0;
627
+ }
628
+ // sync: ohne Ziel-Schalter der ganze Bestand, mit --ort/--harness nur dieses Ziel.
629
+ const nurZiel = werte.has('harness') || werte.has('ort');
630
+ const eintraege = await anwendungsfaelle.bundleAbgleich({
631
+ ...(name === undefined ? {} : { bundle: name }),
632
+ ...(nurZiel ? { ziel } : {}),
633
+ });
634
+ if (eintraege.length === 0)
635
+ console.log('Kein Bundle installiert, nichts abzugleichen.');
636
+ for (const zeile of bundleBericht(eintraege))
637
+ console.log(zeile);
638
+ return 0;
639
+ }
493
640
  if (kommando === 'mcp') {
494
641
  const schalter = schalterLesen(argv.slice(1), 'mcp');
495
642
  unterkommandoPruefen('mcp', schalter);
@@ -568,13 +715,18 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
568
715
  }
569
716
  if (kommando === 'update') {
570
717
  restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
571
- const { checkouts: liste, pluginHarnesses, aufraeumen } = await anwendungsfaelle.aktualisieren();
718
+ const { checkouts: liste, bundles, aufraeumen } = await anwendungsfaelle.aktualisieren({ ausloeser: 'cli' });
572
719
  if (liste.length === 0)
573
720
  console.log('Keine Checkouts.');
574
- for (const { checkout, aktualisierung } of liste) {
721
+ for (const { checkout, aktualisierung, geaenderteSkills } of liste) {
575
722
  const kopf = `${quelleAlsText(checkout.quelle)} @ ${checkout.ref}`;
576
- if (aktualisierung.ergebnis === 'aktualisiert')
723
+ if (aktualisierung.ergebnis === 'aktualisiert') {
577
724
  console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
725
+ // Auf Skill-Ebene berichten (E70): sonst steht da nur, dass die Quelle neue Commits hat.
726
+ // Ohne Treffer bleibt die Zeile weg — der Commit hat dann nichts in einem Skill geändert.
727
+ if (geaenderteSkills.length > 0)
728
+ console.log(` ${zahlwort(geaenderteSkills.length, 'Skill', 'Skills')} geändert: ${geaenderteSkills.join(', ')}`);
729
+ }
578
730
  else if (aktualisierung.ergebnis === 'unveraendert')
579
731
  console.log(` unverändert ${kopf}`);
580
732
  else if (aktualisierung.ergebnis === 'uebersprungen')
@@ -582,8 +734,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
582
734
  else
583
735
  console.log(` fehlgeschlagen ${kopf} ${aktualisierung.fehler}`);
584
736
  }
585
- for (const name of pluginHarnesses)
586
- console.log(` Plugins ${name} aktualisiert (E14)`);
737
+ // Bundles im selben Lauf (E69, F64=O1): eine geänderte Definition wird hier nachgezogen.
738
+ for (const zeile of bundleBericht(bundles))
739
+ console.log(zeile);
587
740
  // Zum Schluss die verwaisten Checkouts: was gelöscht wurde und was liegen bleibt.
588
741
  for (const verwaist of aufraeumen.verwaiste) {
589
742
  const kopf = `${quelleAlsText(verwaist.checkout.quelle)} @ ${verwaist.checkout.ref}`;
@@ -598,11 +751,26 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
598
751
  const geloescht = aufraeumen.verwaiste.filter((v) => v.geloescht).length;
599
752
  console.log(`${aufraeumen.verwaiste.length} verwaist, ${geloescht} gelöscht, ${aufraeumen.verwaiste.length - geloescht} liegen geblieben.`);
600
753
  }
754
+ // Zuletzt hmgr selbst (E77): wer schon aktualisiert, will auch wissen, dass sein hmgr alt ist.
755
+ // Verglichen wird die eigene Version (E72) gegen npm, gemeldet wird nur — installiert wird
756
+ // nichts. Offline oder Registry unerreichbar heißt keine Zeile, still: der Lauf selbst ist
757
+ // fertig und gelungen, ein fehlgeschlagener Nebenblick darf ihn nicht kommentieren.
758
+ const selbst = await cliUpdateHinweis(paketVersion());
759
+ if (selbst !== null) {
760
+ console.log(selbst.text);
761
+ await anwendungsfaelle.hmgrEreignisVermerken({ art: 'cli-verfuegbar', version: selbst.neu }, 'cli');
762
+ }
601
763
  return 0;
602
764
  }
603
765
  if (kommando === 'status') {
604
766
  restPruefen(schalterLesen(argv.slice(1), 'status').rest, 0, 'status');
605
- for (const zielZustand of await anwendungsfaelle.istZustand()) {
767
+ // Ziele inaktiver Harnesses bleiben aus der Liste (E74), auch wenn dort noch Links oder fremde
768
+ // Einträge liegen: was hmgr nicht managed, zeigt es nicht. Gefiltert wird hier, nicht in
769
+ // istZustand — der Kern bleibt ungefiltert (E19), sonst hielte kein Link in einem inaktiven
770
+ // Harness seinen Checkout mehr vor dem Aufräumen.
771
+ const aktiveStatus = new Set(await anwendungsfaelle.aktiveHarnesses());
772
+ const sichtbareZiele = (await anwendungsfaelle.istZustand()).filter((z) => aktiveStatus.has(z.ziel.harness));
773
+ for (const zielZustand of sichtbareZiele) {
606
774
  console.log(`Ziel ${zielZustand.ziel.harness} ${beschreibeOrt(zielZustand.ziel.ort)} (${zielZustand.verzeichnis})`);
607
775
  if (zielZustand.eintraege.length === 0)
608
776
  console.log(' (leer)');
@@ -618,6 +786,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
618
786
  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))}`);
619
787
  }
620
788
  }
789
+ // Bundles des Ziels (E69), unter den Einträgen: welche Links als Einheit zusammengehören.
790
+ for (const bundle of zielZustand.bundles)
791
+ console.log(` Bundle ${bundle.name} (${quelleAlsText(bundle.quelle)}) ${bundle.skills.join(', ')}`);
621
792
  if (zielZustand.plugins !== undefined)
622
793
  console.log(` Plugins: ${zielZustand.plugins.length === 0 ? 'keine' : zielZustand.plugins.join(', ')}`);
623
794
  }
@@ -626,6 +797,18 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
626
797
  console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
627
798
  return 0;
628
799
  }
800
+ if (kommando === 'log') {
801
+ restPruefen(schalterLesen(argv.slice(1), 'log').rest, 0, 'log');
802
+ const eintraege = await anwendungsfaelle.verlauf();
803
+ if (eintraege.length === 0)
804
+ console.log('Noch keine Ereignisse.');
805
+ for (const eintrag of eintraege) {
806
+ console.log(verlaufZeitpunkt(eintrag.zeit));
807
+ for (const ereignis of eintrag.ereignisse)
808
+ console.log(` ${verlaufEreignisZeile(ereignis)}`);
809
+ }
810
+ return 0;
811
+ }
629
812
  if (kommando === 'harness') {
630
813
  const schalter = schalterLesen(argv.slice(1), 'harness');
631
814
  unterkommandoPruefen('harness', schalter);
@@ -679,13 +862,83 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
679
862
  'harness skills-dir <name> add <pfad> oder harness skills-dir <name> remove. ' +
680
863
  'hmgr help harness zeigt Beispiele.');
681
864
  }
865
+ // Homedirs eines Harness (E78): zeigen, das primäre umstellen (F89), zusätzliche in einer
866
+ // WSL-Distro hinzufügen und entfernen (F87).
867
+ if (unterkommando === 'home') {
868
+ const harnessName = rest[1];
869
+ if (harnessName === undefined)
870
+ throw new Error('harness home braucht einen Harness-Namen, etwa hmgr harness home claude-code. ' +
871
+ 'hmgr harness list zeigt die bekannten Namen.');
872
+ const unter = rest[2];
873
+ if (unter === undefined) {
874
+ restPruefen(rest, 2, 'harness', 'home');
875
+ const ansicht = (await anwendungsfaelle.harnessHeime()).find((h) => h.harness === harnessName);
876
+ if (ansicht === undefined)
877
+ throw new HarnessTypUnbekannt(harnessName);
878
+ for (const heim of ansicht.heime) {
879
+ const wie = heim.adresse ?? (heim.eingestellt ? 'home (eingestellt)' : 'home');
880
+ console.log(`${harnessName} ${wie} ${heim.pfad}`);
881
+ }
882
+ if (ansicht.kannZusaetzliche)
883
+ console.log(` Weiteres Homedir: hmgr harness home ${harnessName} add wsl:<distro>.`);
884
+ return 0;
885
+ }
886
+ if (unter === 'set') {
887
+ restPruefen(rest, 4, 'harness', 'home');
888
+ const pfad = rest[3];
889
+ if (pfad === undefined)
890
+ throw new Error('harness home <name> set braucht einen Pfad, etwa hmgr harness home claude-code set D:\\Profile\\tim.');
891
+ const gesetzt = await anwendungsfaelle.harnessHeimSetzen(harnessName, pfad);
892
+ console.log(`${harnessName} liest sein Homedir jetzt unter ${gesetzt}.`);
893
+ console.log(' Was am alten Ort liegt, bleibt dort liegen. hmgr zeigt es nicht mehr.');
894
+ return 0;
895
+ }
896
+ if (unter === 'reset') {
897
+ restPruefen(rest, 3, 'harness', 'home');
898
+ await anwendungsfaelle.harnessHeimZuruecksetzen(harnessName);
899
+ const ansicht = (await anwendungsfaelle.harnessHeime()).find((h) => h.harness === harnessName);
900
+ console.log(`${harnessName} liest sein Homedir wieder unter ${ansicht?.heime[0]?.pfad ?? '?'}.`);
901
+ return 0;
902
+ }
903
+ if (unter === 'add') {
904
+ restPruefen(rest, 4, 'harness', 'home');
905
+ const adresse = rest[3];
906
+ if (adresse === undefined) {
907
+ const distros = await anwendungsfaelle.wslDistros();
908
+ const beispiel = distros[0] ?? 'Ubuntu';
909
+ throw new Error(`harness home <name> add braucht eine Adresse, etwa hmgr harness home ${harnessName} add wsl:${beispiel}.` +
910
+ (distros.length > 0 ? ` Installierte Distros: ${distros.join(', ')}.` : ''));
911
+ }
912
+ const heim = await anwendungsfaelle.harnessHeimHinzufuegen(harnessName, adresse);
913
+ console.log(`${harnessName} ${heim.adresse}: Homedir ${heim.pfad}.`);
914
+ console.log(` Verlinken dorthin mit --harness ${harnessName} --ort ${heim.adresse}.`);
915
+ return 0;
916
+ }
917
+ if (unter === 'remove') {
918
+ restPruefen(rest, 4, 'harness', 'home');
919
+ const adresse = rest[3];
920
+ if (adresse === undefined)
921
+ throw new Error(`harness home <name> remove braucht eine Adresse, etwa hmgr harness home ${harnessName} remove wsl:Ubuntu.`);
922
+ await anwendungsfaelle.harnessHeimEntfernen(harnessName, adresse);
923
+ console.log(`${harnessName} ${adresse}: Homedir vergessen. Die Links dort bleiben liegen.`);
924
+ return 0;
925
+ }
926
+ throw new Error(`Unbekanntes Unterkommando „${unter}" für harness home: harness home <name>, ` +
927
+ 'harness home <name> set <pfad>, harness home <name> reset, harness home <name> add wsl:<distro> ' +
928
+ 'oder harness home <name> remove wsl:<distro>. hmgr help harness zeigt Beispiele.');
929
+ }
682
930
  if (unterkommando === 'list') {
683
931
  const aktive = new Set(await anwendungsfaelle.aktiveHarnesses());
684
932
  const fehlend = new Set(await anwendungsfaelle.nichtVorhandeneHarnesses());
933
+ const heime = await anwendungsfaelle.harnessHeime();
685
934
  for (const name of anwendungsfaelle.harnessTypen()) {
686
935
  const aktivText = aktive.has(name) ? 'aktiv' : 'inaktiv';
687
936
  const vorhandenText = fehlend.has(name) ? 'nicht vorhanden' : 'vorhanden';
688
- console.log(`${name} ${aktivText}, ${vorhandenText}`);
937
+ // Zusätzliche Homedirs (E78) gehören in die Übersicht: sonst sähe niemand, dass ein
938
+ // zweites Ziel je Harness existiert, ohne extra danach zu fragen.
939
+ const zusatz = (heime.find((h) => h.harness === name)?.heime ?? []).filter((h) => h.adresse !== undefined);
940
+ const heimText = zusatz.length === 0 ? '' : `, ${zusatz.map((h) => h.adresse).join(', ')}`;
941
+ console.log(`${name} ${aktivText}, ${vorhandenText}${heimText}`);
689
942
  }
690
943
  return 0;
691
944
  }
@@ -734,10 +987,69 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
734
987
  }
735
988
  return 0;
736
989
  }
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.`);
990
+ throw new Error(`${unterkommando === undefined ? 'harness braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für harness`}: harness list, harness enable <name>, harness disable <name> oder harness home <name>. hmgr help harness zeigt Beispiele.`);
738
991
  }
739
992
  throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
740
993
  }
994
+ /**
995
+ * Bericht über den Bundle-Abgleich (E69), gleich für `hmgr bundle sync` und `hmgr update`: je
996
+ * Bundle eine Zeile mit seinem Ausgang, darunter, was sich geändert hat. „fehlgeschlagen" heißt
997
+ * genau das — der Zusatz sagt, dass das Bundle im Ziel auf dem alten Stand bleibt.
998
+ */
999
+ function bundleBericht(eintraege) {
1000
+ const zeilen = [];
1001
+ for (const eintrag of eintraege) {
1002
+ const kopf = `Bundle ${eintrag.bundle} ${eintrag.ziel.harness} ${beschreibeOrt(eintrag.ziel.ort)}`;
1003
+ if (eintrag.ergebnis === 'fehlgeschlagen') {
1004
+ zeilen.push(` fehlgeschlagen ${kopf} bleibt auf dem alten Stand`);
1005
+ for (const zeile of (eintrag.fehler ?? '').split('\n'))
1006
+ zeilen.push(` ${zeile}`);
1007
+ continue;
1008
+ }
1009
+ zeilen.push(` ${eintrag.ergebnis === 'aktualisiert' ? 'aktualisiert ' : 'unverändert '} ${kopf}`);
1010
+ for (const neu of eintrag.neueQuellen)
1011
+ zeilen.push(` neue Quelle ${quelleAlsText(neu)}`);
1012
+ if (eintrag.gelegt.length > 0)
1013
+ zeilen.push(` gelegt ${eintrag.gelegt.join(', ')}`);
1014
+ if (eintrag.entfernt.length > 0)
1015
+ zeilen.push(` entfernt ${eintrag.entfernt.join(', ')}`);
1016
+ }
1017
+ return zeilen;
1018
+ }
1019
+ /** Kopfzeile eines Verlaufseintrags (E80): lokale Zeit, damit sie zum Rechner des Lesers passt. */
1020
+ function verlaufZeitpunkt(iso) {
1021
+ const datum = new Date(iso);
1022
+ const zwei = (n) => String(n).padStart(2, '0');
1023
+ return (`${zwei(datum.getDate())}.${zwei(datum.getMonth() + 1)}.${datum.getFullYear()} ` +
1024
+ `${zwei(datum.getHours())}:${zwei(datum.getMinutes())}`);
1025
+ }
1026
+ /** Eine Ereigniszeile im Verlauf (E80), eingerückt unter der Kopfzeile ihres Laufs. */
1027
+ function verlaufEreignisZeile(ereignis) {
1028
+ if (ereignis.art === 'checkout') {
1029
+ const stand = `${ereignis.von.slice(0, 7)} → ${ereignis.nach.slice(0, 7)}`;
1030
+ const skills = ereignis.skills.length > 0 ? `, Skills: ${ereignis.skills.join(', ')}` : '';
1031
+ return `${ereignis.quelle} @ ${ereignis.ref}: ${stand}${skills}`;
1032
+ }
1033
+ if (ereignis.art === 'bundle') {
1034
+ const teile = [
1035
+ ereignis.gelegt.length > 0 ? `gelegt ${ereignis.gelegt.join(', ')}` : undefined,
1036
+ ereignis.entfernt.length > 0 ? `entfernt ${ereignis.entfernt.join(', ')}` : undefined,
1037
+ ].filter((t) => t !== undefined);
1038
+ // Formatierung wie in bundleBericht (Harness + beschreibeOrt); ein String-Ziel (vor E80-Nachtrag
1039
+ // geschrieben) bleibt unverändert stehen.
1040
+ const ziel = typeof ereignis.ziel === 'string' ? ereignis.ziel : `${ereignis.ziel.harness} ${beschreibeOrt(ereignis.ziel.ort)}`;
1041
+ return `Bundle ${ereignis.bundle} → ${ziel}: ${teile.join('; ')}`;
1042
+ }
1043
+ if (ereignis.art === 'aufgeraeumt')
1044
+ return `Checkout ${ereignis.checkout} aufgeräumt`;
1045
+ if (ereignis.art === 'fehler')
1046
+ return `Fehler ${ereignis.betrifft}: ${ereignis.fehler}`;
1047
+ if (ereignis.art === 'hmgr-installiert')
1048
+ return ereignis.paket === 'gui' ? `hmgr-GUI ${ereignis.version} installiert` : `hmgr ${ereignis.version} installiert`;
1049
+ if (ereignis.art === 'gui-update-geladen')
1050
+ return `GUI-Update ${ereignis.version} geladen`;
1051
+ return `CLI ${ereignis.version} verfügbar`;
1052
+ }
741
1053
  /** Grund eines Links als Suffix (E37). „manuell" bleibt stumm, damit die Ausgabe ruhig bleibt. */
742
1054
  function beschreibeGrund(grund) {
743
1055
  if (grund === undefined || grund.art === 'manuell')
@@ -752,9 +1064,10 @@ function zahlwort(n, ein, viele) {
752
1064
  function kappen(text, laenge = 80) {
753
1065
  return text.length <= laenge ? text : `${text.slice(0, laenge - 1)}…`;
754
1066
  }
1067
+ /** Ort als Text, so geschrieben, wie `--ort` ihn wieder annimmt. */
755
1068
  function beschreibeOrt(ort) {
756
1069
  if (ort.art === 'home')
757
- return 'home';
1070
+ return ort.heim === undefined ? 'home' : ort.heim;
758
1071
  if (ort.art === 'profil')
759
1072
  return `profil:${ort.name}`;
760
1073
  return `dir:${ort.pfad}`;
@@ -768,12 +1081,19 @@ function istModellFehler(fehler) {
768
1081
  fehler instanceof ZeigerMehrdeutig ||
769
1082
  fehler instanceof ZeigerSchreibenGesperrt ||
770
1083
  fehler instanceof HarnessNichtVorhanden ||
1084
+ fehler instanceof HarnessInaktiv ||
771
1085
  fehler instanceof HarnessTypUnbekannt ||
772
1086
  fehler instanceof KeinPassenderRef ||
773
1087
  fehler instanceof EinschraenkungOhneZeiger ||
774
1088
  fehler instanceof PluginMehrdeutig ||
775
1089
  fehler instanceof PluginUnbekannt ||
776
1090
  fehler instanceof AbhaengigkeitFehlt ||
1091
+ fehler instanceof BundleUnbekannt ||
1092
+ fehler instanceof BundleMehrdeutig ||
1093
+ fehler instanceof BundleMitgliederFehlen ||
1094
+ fehler instanceof BundleNamenskonflikte ||
1095
+ fehler instanceof BundleNichtInstalliert ||
1096
+ fehler instanceof BundleDateiFehler ||
777
1097
  fehler instanceof QuelleUnbekannt ||
778
1098
  fehler instanceof QuelleNameBelegt ||
779
1099
  fehler instanceof QuelleAufraeumenLokaleAenderungen ||
@@ -789,7 +1109,14 @@ function istModellFehler(fehler) {
789
1109
  fehler instanceof McpNurGeteilterFundort ||
790
1110
  fehler instanceof McpUebernahmeNichtMoeglich ||
791
1111
  fehler instanceof McpNichtInGeteilterDatei ||
792
- fehler instanceof McpUebernahmeUnvollstaendig);
1112
+ fehler instanceof McpUebernahmeUnvollstaendig ||
1113
+ fehler instanceof HeimAdresseUngueltig ||
1114
+ fehler instanceof HeimBereitsVorhanden ||
1115
+ fehler instanceof HeimFaehigkeitFehlt ||
1116
+ fehler instanceof HeimNurUnterWindows ||
1117
+ fehler instanceof HeimOhneCliZugriff ||
1118
+ fehler instanceof HeimUnbekannt ||
1119
+ fehler instanceof WslBrueckeFehlgeschlagen);
793
1120
  }
794
1121
  /**
795
1122
  * Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
@@ -808,14 +1135,42 @@ function naechsterSchritt(fehler) {
808
1135
  return 'hmgr plugin list zeigt die Plugins der bekannten Quellen. Fehlt der Marketplace, erst hmgr source add <url>.';
809
1136
  if (fehler instanceof AbhaengigkeitFehlt)
810
1137
  return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
1138
+ if (fehler instanceof BundleUnbekannt)
1139
+ return 'hmgr bundle list zeigt die Bundles der bekannten Quellen. Fehlt die Quelle, erst hmgr source add <url|pfad>.';
1140
+ if (fehler instanceof BundleMehrdeutig)
1141
+ return 'Quelle eingrenzen: --quelle <teilstring der URL oder des Pfads>. hmgr bundle list zeigt, wo welches Bundle steht.';
1142
+ if (fehler instanceof BundleMitgliederFehlen)
1143
+ return 'Das Bundle ist nur ganz oder gar nicht zu haben. Wer nur einen Teil braucht, verlinkt die erreichbaren Skills einzeln mit hmgr link.';
1144
+ if (fehler instanceof BundleNamenskonflikte)
1145
+ return 'Die genannten Namen erst freimachen (hmgr unlink <skill> bzw. hmgr adopt <name>), dann bundle add erneut.';
1146
+ if (fehler instanceof BundleNichtInstalliert)
1147
+ return 'hmgr status zeigt je Ziel, welche Bundles dort installiert sind.';
1148
+ if (fehler instanceof BundleDateiFehler)
1149
+ return 'Die Definition liegt in der Quelle und wird dort korrigiert, hmgr schreibt sie nie. hmgr help bundle zeigt das Format.';
811
1150
  if (fehler instanceof KeinPassenderRef)
812
1151
  return 'Die Tags der Quelle sind frisch geholt, keiner passt. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
813
1152
  if (fehler instanceof HarnessNichtVorhanden)
814
1153
  return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
1154
+ if (fehler instanceof HarnessInaktiv)
1155
+ return `Erst aktivieren: hmgr harness enable ${fehler.harness}. Bestehende Links dort bleiben unberührt.`;
815
1156
  if (fehler instanceof OrtNichtUnterstuetzt)
816
1157
  return 'profil:<name> kennt nur hermes, claude-code und agents kennen home und dir:<pfad>.';
817
1158
  if (fehler instanceof HarnessTypUnbekannt)
818
1159
  return 'Bekannt sind claude-code, hermes und agents. hmgr harness list zeigt sie mit ihrem Stand.';
1160
+ if (fehler instanceof HeimAdresseUngueltig)
1161
+ return 'Ein zusätzliches Homedir wird als wsl:<distro> angegeben. Ein abweichender normaler Pfad gehört an hmgr harness home <name> set <pfad>.';
1162
+ if (fehler instanceof HeimFaehigkeitFehlt)
1163
+ return 'Zusätzliche Homedirs kennt bisher nur claude-code. Für einen abweichenden Pfad: hmgr harness home <name> set <pfad>.';
1164
+ if (fehler instanceof HeimNurUnterWindows)
1165
+ return 'Auf diesem Rechner gibt es kein WSL. Das Homedir wird auf dem Windows-Rechner eingetragen, auf dem die Distro läuft.';
1166
+ if (fehler instanceof HeimBereitsVorhanden)
1167
+ return `hmgr harness home ${fehler.harness} zeigt die eingetragenen Homedirs.`;
1168
+ if (fehler instanceof HeimUnbekannt)
1169
+ return `hmgr harness home ${fehler.harness} zeigt die eingetragenen Homedirs, add trägt eines ein.`;
1170
+ if (fehler instanceof HeimOhneCliZugriff)
1171
+ return 'Skill-Links dort legt hmgr selbst: hmgr link <skill> --harness claude-code --ort <adresse>.';
1172
+ if (fehler instanceof WslBrueckeFehlgeschlagen)
1173
+ return 'wsl -l -q zeigt die installierten Distros. Startet die Distro nicht, hilft wsl --shutdown und ein erneuter Versuch.';
819
1174
  if (fehler instanceof QuelleUnbekannt)
820
1175
  return 'hmgr source list zeigt die bekannten Quellen.';
821
1176
  if (fehler instanceof QuelleNameBelegt)
@@ -0,0 +1,44 @@
1
+ // Die installierte hmgr-CLI aus Sicht der GUI (E77). Die GUI trägt ihre eigene Version in sich
2
+ // (`paketVersion()`, E72) — die CLI daneben ist ein zweites, unabhängig installiertes Paket, ihre
3
+ // Version steht nur über ihren eigenen Aufruf fest.
4
+ import spawn from 'cross-spawn';
5
+ import { versionLesen } from "../kern/version.js";
6
+ /**
7
+ * `hmgr --version` (E72: nur die Nummer, Exit 0). Null heißt „nichts zu melden": keine CLI im PATH,
8
+ * Fehlercode, Timeout, oder eine Ausgabe, die keine Version ist. Kein Fehler dringt nach außen —
9
+ * eine fehlende CLI ist der Normalfall bei einer reinen GUI-Installation, keine Störung.
10
+ *
11
+ * `spawn` von cross-spawn wie im claude-Adapter (E65/B7): unter Windows ist die npm-installierte
12
+ * `hmgr` ein `.cmd`-Shim, das Node seit dem Fix zu CVE-2024-27980 ohne Umweg über cmd.exe nicht
13
+ * mehr startet. stdin zu und ein Timeout, damit nichts hängen bleibt.
14
+ */
15
+ export function hmgrCliVersion() {
16
+ return new Promise((aufloesen) => {
17
+ let fertig = false;
18
+ const melden = (wert) => {
19
+ if (fertig)
20
+ return;
21
+ fertig = true;
22
+ aufloesen(wert);
23
+ };
24
+ try {
25
+ const kind = spawn('hmgr', ['--version'], {
26
+ stdio: ['ignore', 'pipe', 'ignore'],
27
+ timeout: 10_000,
28
+ killSignal: 'SIGKILL',
29
+ });
30
+ let stdout = '';
31
+ kind.stdout.on('data', (stueck) => (stdout += stueck));
32
+ kind.on('error', () => melden(null));
33
+ kind.on('close', (code) => {
34
+ if (code !== 0)
35
+ return melden(null);
36
+ const text = stdout.trim().split('\n')[0]?.trim() ?? '';
37
+ melden(versionLesen(text) === null ? null : text);
38
+ });
39
+ }
40
+ catch {
41
+ melden(null);
42
+ }
43
+ });
44
+ }