hmgr 0.0.7 → 0.0.14

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.
@@ -0,0 +1,212 @@
1
+ // Die Bridge in eine WSL-Distro (E78): `wsl.exe -d <distro> <linux-kommando>`. Über `\\wsl$` geht
2
+ // nichts davon — Windows legt dort keine Links an und liest durch Linux-Symlinks nicht hindurch
3
+ // (gemessen, HARNESS-FAKTEN „Links zwischen Windows und WSL").
4
+ import spawn from 'cross-spawn';
5
+ import path from 'node:path';
6
+ import { WslBrueckeFehlgeschlagen } from "../kern/modell.js";
7
+ /**
8
+ * Standard-Ausführer: `wsl.exe` vom PATH, über cross-spawn wie die claude-CLI (E65/B7). stdin ist
9
+ * zu, sonst hängt eine Distro, die auf Eingabe wartet; der Timeout fängt alles Übrige. Ein
10
+ * Fehlschlag wird zum Code, nicht zur Ausnahme.
11
+ */
12
+ export const wslAusfuehrer = (args) => new Promise((aufloesen) => {
13
+ const kind = spawn('wsl.exe', args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, killSignal: 'SIGKILL' });
14
+ const teile = [];
15
+ const fehlerTeile = [];
16
+ kind.stdout.on('data', (d) => teile.push(d));
17
+ kind.stderr.on('data', (d) => fehlerTeile.push(d));
18
+ kind.on('error', (fehler) => aufloesen({ ausgabe: Buffer.from(fehler.message, 'utf8'), code: 1 }));
19
+ kind.on('close', (code) => aufloesen({ ausgabe: Buffer.concat(code === 0 ? teile : [...teile, ...fehlerTeile]), code: code ?? 1 }));
20
+ });
21
+ /**
22
+ * Eine wsl.exe-eigene Ausgabe als Text (E78, gemessen): UTF-16LE, mit BOM. Node kennt die Kodierung
23
+ * als „utf16le"; die BOM bleibt beim Dekodieren als `\uFEFF` stehen und wird hier entfernt. Ein
24
+ * ungerades Byte am Ende (abgeschnittene Ausgabe) fällt weg statt ein Ersatzzeichen zu ergeben.
25
+ */
26
+ export function utf16Text(ausgabe) {
27
+ const gerade = ausgabe.length % 2 === 0 ? ausgabe : ausgabe.subarray(0, ausgabe.length - 1);
28
+ return gerade.toString('utf16le').replace(/^\uFEFF/, '');
29
+ }
30
+ /**
31
+ * Die Distronamen aus `wsl -l -q` (E78): eine je Zeile, UTF-16LE. Leerzeilen und Reste des
32
+ * Zeilenumbruchs fallen weg. Der Standard-Distro hängt in anderen Listenformen ein „(Standard)" an,
33
+ * `-q` liefert nur die nackten Namen.
34
+ */
35
+ export function distroListeLesen(ausgabe) {
36
+ return utf16Text(ausgabe)
37
+ .split(/\r?\n/)
38
+ .map((zeile) => zeile.trim())
39
+ .filter((zeile) => zeile.length > 0);
40
+ }
41
+ /**
42
+ * Ein Wert für die einfach zitierte Form der Shell: alles bleibt wörtlich, ein enthaltenes
43
+ * Apostroph wird ausgeklammert. Ohne das zerfiele ein Pfad mit Leerzeichen in mehrere Argumente,
44
+ * sobald wsl.exe die Kommandozeile wieder zusammensetzt.
45
+ */
46
+ export function shZitat(text) {
47
+ return `'${text.replace(/'/g, `'\\''`)}'`;
48
+ }
49
+ /**
50
+ * Ein POSIX-Pfad in die Windows-Schreibweise (E78): `/mnt/c/work/quelle` wird `C:\work\quelle`.
51
+ * `wurzel` ist die Automount-Wurzel der Distro, üblich `/mnt`, aber über `[automount] root`
52
+ * einstellbar — deshalb wird sie einmal per `wslpath` ermittelt statt geraten. Ein Pfad außerhalb
53
+ * (im Linux-Dateisystem) hat keine Windows-Entsprechung: `undefined`.
54
+ */
55
+ export function wslNachWindows(pfad, wurzel) {
56
+ const basis = wurzel.endsWith('/') ? wurzel.slice(0, -1) : wurzel;
57
+ const treffer = new RegExp(`^${basis.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/([a-zA-Z])(/.*)?$`).exec(pfad);
58
+ if (treffer === null)
59
+ return undefined;
60
+ const rest = (treffer[2] ?? '').replace(/\//g, '\\');
61
+ return `${treffer[1].toUpperCase()}:${rest.length === 0 ? '\\' : rest}`;
62
+ }
63
+ /**
64
+ * Die Gegenrichtung: `C:\work\quelle` wird `/mnt/c/work/quelle`. Ein Pfad ohne Laufwerksbuchstaben
65
+ * (UNC, relativ) hat keine Entsprechung unter der Automount-Wurzel: `undefined`.
66
+ */
67
+ export function windowsNachWsl(pfad, wurzel) {
68
+ const treffer = /^([a-zA-Z]):[\\/](.*)$/.exec(pfad);
69
+ if (treffer === null)
70
+ return undefined;
71
+ const basis = wurzel.endsWith('/') ? wurzel.slice(0, -1) : wurzel;
72
+ const rest = treffer[2].replace(/\\/g, '/').replace(/\/+$/, '');
73
+ return `${basis}/${treffer[1].toLowerCase()}${rest.length === 0 ? '' : `/${rest}`}`;
74
+ }
75
+ /**
76
+ * Eine WSL-Distro über die Bridge. Jede Dateisystem-Operation läuft als `/bin/sh -c "<skript>"` in
77
+ * der Distro; Pfade gehen einfach zitiert hinein (`shZitat`), weil wsl.exe die Kommandozeile wieder
78
+ * zusammensetzt und ein Leerzeichen sonst trennte.
79
+ */
80
+ export class WslBruecke {
81
+ distro;
82
+ ausfuehren;
83
+ /** Automount-Wurzel, einmal je Lauf ermittelt: ein `wslpath`-Aufruf statt einem je Linkziel. */
84
+ wurzelVersprechen;
85
+ constructor(distro, ausfuehren = wslAusfuehrer) {
86
+ this.distro = distro;
87
+ this.ausfuehren = ausfuehren;
88
+ }
89
+ /** Ein Linux-Kommando in der Distro, Ausgabe als UTF-8. Wirft, wenn die Bridge selbst scheitert. */
90
+ async sh(skript) {
91
+ const { ausgabe, code } = await this.ausfuehren(['-d', this.distro, '/bin/sh', '-c', skript]);
92
+ const text = ausgabe.toString('utf8');
93
+ if (code !== 0)
94
+ throw new WslBrueckeFehlgeschlagen(this.distro, text);
95
+ return text;
96
+ }
97
+ /** Dasselbe, aber ein Fehlschlag ist kein Fehler: `undefined`. Für lesende Abfragen. */
98
+ async shWeich(skript) {
99
+ const { ausgabe, code } = await this.ausfuehren(['-d', this.distro, '/bin/sh', '-c', skript]);
100
+ return code === 0 ? ausgabe.toString('utf8') : undefined;
101
+ }
102
+ /**
103
+ * Das Home des Standardbenutzers der Distro (F87). wsl.exe startet als dieser Benutzer — derselbe,
104
+ * der Claude Code dort interaktiv nutzt; eine Benutzer-Option gibt es deshalb nicht.
105
+ */
106
+ async home() {
107
+ const text = (await this.sh('echo $HOME')).trim();
108
+ if (text.length === 0)
109
+ throw new WslBrueckeFehlgeschlagen(this.distro, 'kein $HOME in der Distro');
110
+ return text;
111
+ }
112
+ /**
113
+ * Die Automount-Wurzel, aus `wslpath -u 'C:\'`. wslpath kommt von WSL selbst und liegt auch in
114
+ * einer Minimal-Distro (gemessen). Schlägt der Aufruf fehl, gilt der Standard `/mnt`: eine
115
+ * Übersetzung mit der üblichen Wurzel ist besser als gar keine.
116
+ */
117
+ async wurzel() {
118
+ this.wurzelVersprechen ??= (async () => {
119
+ const text = await this.shWeich(`wslpath -u ${shZitat('C:\\')}`);
120
+ const pfad = text?.trim() ?? '';
121
+ const treffer = /^(.*)\/[a-zA-Z]\/?$/.exec(pfad);
122
+ return treffer?.[1] !== undefined && treffer[1].length > 0 ? treffer[1] : '/mnt';
123
+ })();
124
+ return this.wurzelVersprechen;
125
+ }
126
+ /** Einen Pfad der Distro in die Schreibweise des Windows-Rechners. Ohne Entsprechung: unverändert. */
127
+ async nachHost(pfad) {
128
+ return wslNachWindows(pfad, await this.wurzel()) ?? pfad;
129
+ }
130
+ /** Die Gegenrichtung. Ohne Entsprechung: unverändert — der Link zeigt dann eben ins Leere. */
131
+ async vomHost(pfad) {
132
+ return windowsNachWsl(pfad, await this.wurzel()) ?? pfad;
133
+ }
134
+ }
135
+ /** Die installierten Distros (`wsl -l -q`). Ohne wsl.exe oder ohne Distro: leere Liste. */
136
+ export async function wslDistros(ausfuehren = wslAusfuehrer) {
137
+ const { ausgabe, code } = await ausfuehren(['-l', '-q']);
138
+ if (code !== 0)
139
+ return [];
140
+ return distroListeLesen(ausgabe);
141
+ }
142
+ /**
143
+ * Zugriff auf ein Zielverzeichnis in einer WSL-Distro (E78). Jede Operation ist ein Aufruf über die
144
+ * Bridge; Pfade sind POSIX-Pfade der Distro, Linkziele übersetzt der Zugriff selbst nach `/mnt/c`
145
+ * und zurück. Ein Symlink im WSL-Home darf auf `/mnt/c/…` zeigen — die Distro liest selbst
146
+ * hindurch, also bleiben die vorhandenen Windows-Checkouts die Linkziele (gemessen).
147
+ */
148
+ export class WslZugriff {
149
+ trenner = '/';
150
+ bruecke;
151
+ constructor(bruecke) {
152
+ this.bruecke = bruecke;
153
+ }
154
+ verbinden(verzeichnis, name) {
155
+ return path.posix.join(verzeichnis, name);
156
+ }
157
+ /**
158
+ * `ls -1A` statt `find -printf`: letzteres ist eine GNU-Erweiterung und fehlt in einer
159
+ * Busybox-Distro. Ein Name mit Zeilenumbruch zerfiele dabei — bei Skill-Ordnern kommt das nicht
160
+ * vor, und der Preis wäre ein Sonderformat für einen Fall, den es nicht gibt.
161
+ */
162
+ async namen(verzeichnis) {
163
+ const text = await this.weich(`ls -1A ${shZitat(verzeichnis)} 2>/dev/null`);
164
+ if (text === undefined)
165
+ return [];
166
+ return text
167
+ .split('\n')
168
+ .map((zeile) => zeile.replace(/\r$/, ''))
169
+ .filter((zeile) => zeile.length > 0);
170
+ }
171
+ /**
172
+ * Drei Ja/Nein-Fragen in einem Aufruf: gibt es den Eintrag (auch als toten Link), ist er ein Link,
173
+ * zeigt er auf ein Verzeichnis. `-d` folgt dem Link, `-L` nicht — genau die Aufteilung, die der
174
+ * lokale Zugriff mit lstat und stat trifft.
175
+ */
176
+ async info(pfad) {
177
+ const p = shZitat(pfad);
178
+ const text = await this.weich(`if [ -e ${p} ] || [ -L ${p} ]; then printf e; fi; if [ -L ${p} ]; then printf l; fi; if [ -d ${p} ]; then printf d; fi`);
179
+ if (text === undefined || !text.includes('e'))
180
+ return undefined;
181
+ return { istLink: text.includes('l'), istVerzeichnis: text.includes('d') };
182
+ }
183
+ async linkZiel(pfad) {
184
+ const text = await this.weich(`readlink ${shZitat(pfad)}`);
185
+ if (text === undefined)
186
+ return undefined;
187
+ const ziel = text.trim();
188
+ if (ziel.length === 0)
189
+ return undefined;
190
+ // Ein relatives Linkziel gibt es bei hmgr-Links nicht, aber ein von Hand gelegter Link könnte
191
+ // eines haben: erst gegen das Verzeichnis auflösen, dann übersetzen.
192
+ const absolut = ziel.startsWith('/') ? ziel : path.posix.resolve(path.posix.dirname(pfad), ziel);
193
+ return await this.bruecke.nachHost(absolut);
194
+ }
195
+ async verzeichnisAnlegen(pfad) {
196
+ await this.bruecke.sh(`mkdir -p ${shZitat(pfad)}`);
197
+ }
198
+ async linkAnlegen(pfad, hostZiel) {
199
+ const ziel = await this.bruecke.vomHost(hostZiel);
200
+ await this.bruecke.sh(`ln -sfn ${shZitat(ziel)} ${shZitat(pfad)}`);
201
+ }
202
+ async linkEntfernen(pfad) {
203
+ await this.bruecke.sh(`rm -f ${shZitat(pfad)}`);
204
+ }
205
+ async loeschen(pfad) {
206
+ await this.bruecke.sh(`rm -rf ${shZitat(pfad)}`);
207
+ }
208
+ /** Lesende Abfragen brechen nicht ab, wenn die Distro schweigt: sie ergeben dann „nichts da". */
209
+ async weich(skript) {
210
+ return await this.bruecke.sh(skript).catch(() => undefined);
211
+ }
212
+ }
@@ -1,8 +1,29 @@
1
1
  // Lokaler Zustand als eine JSON-Datei, Standard ~/.hmgr/state.json (E29).
2
+ import { readFileSync } from 'node:fs';
2
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { ZUSTAND_FASSUNG, leererZustand, } from "../ports/zustand.js";
5
6
  import { ohneBom } from "./datei-text.js";
7
+ /** Der gelesene Text als Zustand. Fehlende Felder bekommen den Wert, der dem Bestand entspricht. */
8
+ function deuten(roh) {
9
+ const daten = JSON.parse(ohneBom(roh));
10
+ return {
11
+ // Fehlt das Feld (state.json vor E69), ist es die erste Fassung: alles ohne Bundles.
12
+ // Geschrieben wird danach in der aktuellen Fassung, ein Rückweg ist nicht vorgesehen.
13
+ fassung: typeof daten.fassung === 'number' ? daten.fassung : 1,
14
+ quellen: (daten.quellen ?? []),
15
+ arbeitsverzeichnisse: (daten.arbeitsverzeichnisse ?? []),
16
+ bindungen: (daten.bindungen ?? []),
17
+ // Fehlt das Feld (state.json vor E38), gilt der bisherige Ist-Zustand: nur claude-code aktiv.
18
+ aktiveHarnesses: (daten.aktiveHarnesses ?? ['claude-code']),
19
+ // Fehlt das Feld (state.json vor E43), gilt: nichts ignoriert.
20
+ ignorierteEintraege: (daten.ignorierteEintraege ?? []),
21
+ // Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert.
22
+ bundleInstallationen: (daten.bundleInstallationen ?? []),
23
+ // Fehlt das Feld (state.json vor E78), gilt: jeder Harness hat nur sein Standard-Homedir.
24
+ harnessHeime: (daten.harnessHeime ?? []),
25
+ };
26
+ }
6
27
  export class ZustandJson {
7
28
  pfad;
8
29
  constructor(pfad) {
@@ -12,21 +33,21 @@ export class ZustandJson {
12
33
  const roh = await readFile(this.pfad, 'utf8').catch(() => undefined);
13
34
  if (roh === undefined)
14
35
  return leererZustand();
15
- const daten = JSON.parse(ohneBom(roh));
16
- return {
17
- // Fehlt das Feld (state.json vor E69), ist es die erste Fassung: alles ohne Bundles.
18
- // Geschrieben wird danach in der aktuellen Fassung, ein Rückweg ist nicht vorgesehen.
19
- fassung: typeof daten.fassung === 'number' ? daten.fassung : 1,
20
- quellen: (daten.quellen ?? []),
21
- arbeitsverzeichnisse: (daten.arbeitsverzeichnisse ?? []),
22
- bindungen: (daten.bindungen ?? []),
23
- // Fehlt das Feld (state.json vor E38), gilt der bisherige Ist-Zustand: nur claude-code aktiv.
24
- aktiveHarnesses: (daten.aktiveHarnesses ?? ['claude-code']),
25
- // Fehlt das Feld (state.json vor E43), gilt: nichts ignoriert.
26
- ignorierteEintraege: (daten.ignorierteEintraege ?? []),
27
- // Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert.
28
- bundleInstallationen: (daten.bundleInstallationen ?? []),
29
- };
36
+ return deuten(roh);
37
+ }
38
+ /**
39
+ * Derselbe Zustand, synchron gelesen (E78). Nötig für die Auflösung der Homedirs: sie hängt an
40
+ * `HarnessTyp.zielVerzeichnis`, und das ist synchron. Ein kaputter oder fehlender Zustand ergibt
41
+ * hier den leeren — die Homedir-Auflösung darf keine Ansicht abbrechen, sie fällt dann auf das
42
+ * Home des Nutzers zurück. Nicht Teil von `ZustandPort`: dort bleibt alles asynchron.
43
+ */
44
+ lesenSync() {
45
+ try {
46
+ return deuten(readFileSync(this.pfad, 'utf8'));
47
+ }
48
+ catch {
49
+ return leererZustand();
50
+ }
30
51
  }
31
52
  async schreiben(zustand) {
32
53
  await mkdir(path.dirname(this.pfad), { recursive: true });
package/dist/cli/hilfe.js CHANGED
@@ -33,9 +33,11 @@ hmgr <kommando> [schalter]
33
33
  sync [pfad] Ziel auf den Stand seines Zeigers bringen
34
34
  update Alle Checkouts aktualisieren
35
35
  status Ist-Zustand der Ziele
36
+ log Verlauf der letzten Läufe (Changelog)
36
37
  harness list Harness-Typen mit Stand aktiv/inaktiv und vorhanden
37
38
  harness enable <name> Harness-Typ aktivieren, hmgr richtet ihn künftig ein
38
39
  harness disable <name> Harness-Typ deaktivieren, mit Angebot zum Säubern
40
+ harness home <name> Homedirs zeigen, umstellen oder ein zusätzliches eintragen
39
41
  harness skills-dir <name> Verwalteten Skill-Ordner zeigen, festlegen oder entfernen
40
42
  --version Versionsnummer von hmgr ausgeben
41
43
 
@@ -128,7 +130,8 @@ Arbeitsverzeichnis mit Zeiger bricht link ohne --local ab, der Link wird nicht g
128
130
 
129
131
  Schalter:
130
132
  --harness <name> claude-code, hermes oder agents, Standard claude-code
131
- --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
133
+ --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
134
+ kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
132
135
  --quelle <teil> Teilstring der Quelle, wenn der Skill-Name in mehreren Quellen vorkommt
133
136
  --version <spanne> Versionseinschränkung nach Composer-Regeln (etwa ^1.2) oder ein Tag- oder
134
137
  Branchname. Nur mit Zeiger, also dir:<pfad> ohne --local (E33)
@@ -146,8 +149,10 @@ Häufige Fälle:
146
149
  „liegt bereits ein fremder Eintrag": --ersetzen setzen oder vorher hmgr adopt <name>.
147
150
  „gibt es keinen Zeiger": --version braucht ein Arbeitsverzeichnis mit composer.json und kein
148
151
  --local. Ohne Zeiger den Link ohne --version legen.
152
+ „ist deaktiviert, hmgr legt dort keine Links an": erst hmgr harness enable <name>, dann erneut
153
+ link. Bestehende Links im deaktivierten Harness bleiben liegen (E76).
149
154
 
150
- Siehe auch: hmgr help unlink, hmgr help sync, hmgr help adopt`;
155
+ Siehe auch: hmgr help unlink, hmgr help sync, hmgr help adopt, hmgr help harness`;
151
156
  const UNLINK = `hmgr unlink <skill> [schalter]
152
157
 
153
158
  Entfernt den Link aus dem Ziel und, wenn das Ziel an einen Zeiger gebunden ist, den Skill aus dem
@@ -158,7 +163,8 @@ Arbeitsverzeichnis mit Zeiger bricht unlink ohne --local ab, der Link bleibt lie
158
163
 
159
164
  Schalter:
160
165
  --harness <name> claude-code, hermes oder agents, Standard claude-code
161
- --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
166
+ --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
167
+ kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
162
168
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
163
169
  --local nur den Link entfernen, den Eintrag im Zeiger stehen lassen
164
170
 
@@ -184,7 +190,8 @@ Probelauf bleibt frei, er schreibt ohnehin nichts.
184
190
 
185
191
  Schalter:
186
192
  --harness <name> claude-code, hermes oder agents, Standard claude-code
187
- --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
193
+ --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
194
+ kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
188
195
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
189
196
  --local Übernahme nicht in den Zeiger schreiben
190
197
  --probelauf nur melden, nichts schreiben
@@ -217,7 +224,9 @@ verlinkt oder entfernt.
217
224
 
218
225
  Schalter (add und remove):
219
226
  --harness <name> claude-code, hermes oder agents, Standard claude-code
220
- --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
227
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes.
228
+ Ein zusätzliches Homedir (wsl:<distro>) kennt die claude-CLI nicht, dort
229
+ gibt es keine Plugins (E78)
221
230
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
222
231
  --local Plugin nicht in den Zeiger schreiben
223
232
  --quelle <teil> nur bei add: Quelle eingrenzen, wenn der Name in mehreren Marketplaces steht
@@ -297,7 +306,8 @@ composer.json. --local gibt es deshalb nicht, und die Sperre aus E57 trifft bund
297
306
 
298
307
  Schalter (add, remove und sync):
299
308
  --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
309
+ --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
310
+ kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
301
311
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
302
312
  --quelle <teil> nur bei add: Quelle eingrenzen, wenn den Namen mehrere Quellen führen
303
313
 
@@ -314,8 +324,10 @@ Häufige Fälle:
314
324
  meist eine Quelle ohne Zugriff oder ein Pfad, an dem kein Skill (mehr) liegt.
315
325
  „lässt sich in … nicht anlegen": die Meldung listet alle Namen, die am Ziel schon belegt sind.
316
326
  „kein Bundle … installiert": hmgr status zeigt je Ziel seine Bundles.
327
+ „ist deaktiviert, hmgr legt dort keine Links an": bundle add braucht einen aktiven Harness, erst
328
+ hmgr harness enable <name>. bundle sync und bundle remove trifft die Sperre nicht (E76).
317
329
 
318
- Siehe auch: hmgr help link, hmgr help plugin, hmgr help status`;
330
+ Siehe auch: hmgr help link, hmgr help plugin, hmgr help status, hmgr help harness`;
319
331
  const MCP = `hmgr mcp add <name> [schalter]
320
332
  hmgr mcp remove <name> [schalter]
321
333
  hmgr mcp migrate <name> [schalter]
@@ -459,6 +471,16 @@ gemeldet. Ein verwaister Checkout mit lokaler Änderung wird nie gelöscht (E15)
459
471
  So wird eine Änderung an einem Skill überall wirksam: in der Quelle committen und pushen, dann
460
472
  hmgr update.
461
473
 
474
+ Zum Schluss vergleicht update die eigene Version mit der auf npm veröffentlichten (E77). Ist dort
475
+ eine neuere, nennt eine letzte Zeile sie und den Befehl dazu (npm install -g hmgr@latest);
476
+ installiert wird nichts von selbst. Ohne Netz oder ohne neuere Version bleibt die Zeile weg.
477
+
478
+ Ergibt der Lauf mindestens ein Ereignis (aktualisierter Checkout, nachgezogenes Bundle,
479
+ aufgeräumter Checkout, ein Fehler), schreibt update denselben Eintrag auch in ~/.hmgr/verlauf.json
480
+ (E80) — dieselbe Datei, in die auch die GUI schreibt. hmgr log zeigt das Changelog. Plugins stehen
481
+ nicht im Verlauf: „claude plugin marketplace update" liefert keine Auskunft, ob sich etwas geändert
482
+ hat, eine Zeile dazu wäre geschönt.
483
+
462
484
  Schalter: keine.
463
485
 
464
486
  Beispiel:
@@ -471,10 +493,35 @@ Häufige Fälle:
471
493
  „liegen lassen … ohne Bestätigung nicht gelöscht": die Frage kam ohne Ja zurück, etwa in einer
472
494
  Pipe oder einem Composer-Script. In einem Terminal erneut hmgr update aufrufen.
473
495
 
474
- Siehe auch: hmgr help sync, hmgr help status`;
496
+ Siehe auch: hmgr help sync, hmgr help status, hmgr help log`;
497
+ const LOG = `hmgr log
498
+
499
+ Zeigt den Verlauf der letzten Läufe aus ~/.hmgr/verlauf.json (E80), neueste zuerst: dieselbe Datei,
500
+ die hmgr update schreibt und die GUI bei jedem automatischen und angestoßenen Lauf schreibt und
501
+ liest. Ein Lauf ohne Ereignis (nichts Neues) steht nicht drin, die Datei ist ein Changelog, keine
502
+ vollständige Historie — sie hält die letzten 20 Einträge.
503
+
504
+ Je Eintrag steht eine Kopfzeile mit Datum und Uhrzeit, darunter je Ereignis eine Zeile: ein
505
+ aktualisierter Checkout mit Quelle, Ref und Commit-Stand samt geänderten Skills (E70), ein
506
+ nachgezogenes Bundle mit gelegten und entfernten Links, ein aufgeräumter (gelöschter) Checkout, ein
507
+ Fehler zu einer Quelle oder einem Bundle, dazu hmgr-Ereignisse: ein Versionssprung von hmgr selbst
508
+ (GUI oder CLI, erkannt beim Start bzw. bei jedem CLI-Aufruf) oder ein gefundenes Update, das noch
509
+ nicht installiert ist. Plugins stehen nicht im Verlauf (siehe hmgr help update).
510
+
511
+ Der Verlauf ist je Rechner: mehrere Maschinen führen ihre eigene Datei, es gibt kein Zusammenführen.
512
+
513
+ Schalter: keine.
514
+
515
+ Beispiel:
516
+ hmgr log
517
+
518
+ Häufige Fälle:
519
+ „Noch keine Ereignisse": entweder frisch installiert, oder jeder bisherige Lauf war ohne Fund.
520
+
521
+ Siehe auch: hmgr help update, hmgr help status`;
475
522
  const STATUS = `hmgr status
476
523
 
477
- Liest den Ist-Zustand vom Dateisystem: je Ziel eines vorhandenen Harness die Einträge mit Quelle
524
+ Liest den Ist-Zustand vom Dateisystem: je Ziel eines vorhandenen, aktiven Harness die Einträge mit Quelle
478
525
  und Ref, davor die Versionseinschränkung, falls der Zeiger eine nennt. Dahinter steht der
479
526
  abgeleitete Grund des Links: das Plugin, zu dem er gehört, oder „über Zeiger", von Hand gelegte
480
527
  Links bleiben ohne Zusatz. Fremde Einträge und Links ohne bekannte Quelle sind eigens
@@ -492,11 +539,18 @@ Häufige Fälle:
492
539
  Zustand steht. Mit hmgr source add <url> die Quelle wieder eintragen.
493
540
  „Nicht vorhanden": der Harness hat sein Verzeichnis auf diesem Rechner nicht angelegt, hmgr
494
541
  überspringt seine Ziele.
542
+ Ein Ziel fehlt: Ziele inaktiver Harness-Typen zeigt status nicht, auch wenn dort noch Links
543
+ liegen. hmgr harness list zeigt den Stand, hmgr harness enable <name> holt die Ziele zurück.
495
544
 
496
545
  Siehe auch: hmgr help adopt, hmgr help sync`;
497
546
  const HARNESS = `hmgr harness list
498
547
  hmgr harness enable <name>
499
548
  hmgr harness disable <name>
549
+ hmgr harness home <name>
550
+ hmgr harness home <name> set <pfad>
551
+ hmgr harness home <name> reset
552
+ hmgr harness home <name> add wsl:<distro>
553
+ hmgr harness home <name> remove wsl:<distro>
500
554
  hmgr harness skills-dir <name> [--profil <name>]
501
555
  hmgr harness skills-dir <name> add <pfad> [--profil <name>]
502
556
  hmgr harness skills-dir <name> remove [--profil <name>]
@@ -509,9 +563,27 @@ alle bekannten Harness-Typen mit ihrem Stand: aktiv oder inaktiv, dazu vorhanden
509
563
 
510
564
  disable säubert nicht von selbst: liegen noch eigene Links oder eine harness-eigene Konfiguration wie
511
565
  Hermes' external_dirs-Eintrag, fragt hmgr, ob sie entfernt werden sollen. Ohne Terminal, etwa in einem
512
- Script, gilt Nein und die Konfiguration bleibt liegen. Ein Link mit ausdrücklich genanntem --harness
513
- (hmgr link, hmgr plugin add) legt weiterhin an jedem vorhandenen Harness an, aktiv oder nicht, das ist
514
- Sache des Nutzers.
566
+ Script, gilt Nein und die Konfiguration bleibt liegen. In ein Ziel eines inaktiven Harness legt hmgr
567
+ keine Links mehr an (E76): hmgr link und hmgr bundle add brechen ab und nennen hmgr harness enable
568
+ <name> als Weg, auch mit ausdrücklich genanntem --harness. Bestehende Links bleiben liegen, hmgr sync
569
+ und hmgr unlink trifft die Sperre nicht.
570
+
571
+ home zeigt die Homedirs eines Harness (E78). set stellt das primäre um: hmgr merkt sich nur den neuen
572
+ Pfad und zeigt ab dann ihn, was am alten Ort liegt, bleibt unberührt liegen — wer umziehen will,
573
+ verlinkt am neuen Ort neu, einen Umzugsassistenten gibt es nicht. reset nimmt die Umstellung zurück,
574
+ ebenfalls ohne etwas zu verschieben.
575
+
576
+ add trägt ein zusätzliches Homedir ein, heute nur eines in einer WSL-Distro: Adresse wsl:<distro>,
577
+ ohne Benutzer-Option, denn wsl.exe startet als Standardbenutzer der Distro — derselbe, der Claude Code
578
+ dort nutzt. Das Home wird einmal ermittelt (wsl -d <distro> /bin/sh -c 'echo $HOME'), angezeigt und
579
+ gespeichert. Jedes Homedir ist danach ein eigenständiges Ziel: es steht als eigene Zeile im status und
580
+ wird mit --ort wsl:<distro> angesprochen. Gespiegelt wird nichts, jedes Ziel wird einzeln verlinkt.
581
+ Links dort legt hmgr über wsl.exe an; das Linkziel bleibt der vorhandene Windows-Checkout, übersetzt
582
+ nach /mnt/c. MCP-Server und Plugins gibt es dort nicht: die claude-CLI dieses Rechners bedient nur das
583
+ primäre Homedir. remove lässt hmgr das Homedir vergessen, die Links dort bleiben liegen.
584
+
585
+ Zusätzliche Homedirs kennt bisher nur claude-code, und nur unter Windows. Steht eines im Zustand,
586
+ etwa weil er von einem Windows-Rechner kommt, zeigt hmgr sein Ziel auf anderen Plattformen nicht.
515
587
 
516
588
  skills-dir zeigt, legt fest oder entfernt den Ordner, aus dem Hermes externe Skills liest
517
589
  (skills.external_dirs in seiner config.yaml, E13, E56). hmgr verwaltet je Ziel (Home oder ein Profil
@@ -531,14 +603,28 @@ Beispiele:
531
603
  hmgr harness list
532
604
  hmgr harness enable hermes
533
605
  hmgr harness disable hermes
606
+ hmgr harness home claude-code
607
+ hmgr harness home claude-code set D:\\Profile\\tim
608
+ hmgr harness home claude-code add wsl:Ubuntu
609
+ hmgr link netzwerk --harness claude-code --ort wsl:Ubuntu
610
+ hmgr harness home claude-code remove wsl:Ubuntu
534
611
  hmgr harness skills-dir hermes
535
612
  hmgr harness skills-dir hermes add ~/.hermes/external-skills
536
613
  hmgr harness skills-dir hermes remove --profil researcher
537
614
 
538
615
  Häufige Fälle:
539
616
  „ist kein bekannter Harness-Typ": hmgr harness list zeigt die bekannten Namen.
617
+ „ist deaktiviert, hmgr legt dort keine Links an": hmgr harness enable <name>, dann erneut link
618
+ bzw. bundle add (E76).
540
619
  Konfiguration bleibt nach disable liegen, wenn die Rückfrage mit Nein oder ohne Terminal beantwortet
541
- wird. Ein erneutes hmgr harness disable <name> bietet das Säubern wieder an.
620
+ wird. hmgr status zeigt die Ziele des Typs dann nicht mehr (E74), sie erscheinen nach hmgr harness
621
+ enable <name> wieder. Ein erneutes hmgr harness disable <name> bietet das Säubern wieder an.
622
+ „ist keine Adresse für ein zusätzliches Homedir": add nimmt wsl:<distro>. Ein abweichender
623
+ normaler Pfad gehört an set, nicht an add.
624
+ „kennt keine zusätzlichen Homedirs": add und remove kennen heute nur claude-code.
625
+ „gibt es nur unter Windows": das WSL-Homedir wird auf dem Windows-Rechner eingetragen, auf dem
626
+ die Distro läuft.
627
+ „wsl.exe -d <distro> ließ sich nicht ausführen": wsl -l -q zeigt die installierten Distros.
542
628
  „kennt keinen verwalteten Skill-Ordner": skills-dir kennt heute nur hermes.
543
629
  „ist schon ein Skill-Ordner verwaltet": erst hmgr harness skills-dir <name> remove, dann add.
544
630
  „ist kein Skill-Ordner verwaltet": remove kennt nur einen vorhandenen Eintrag, add legt ihn fest.
@@ -559,6 +645,7 @@ const THEMEN = new Map([
559
645
  ['sync', SYNC],
560
646
  ['update', UPDATE],
561
647
  ['status', STATUS],
648
+ ['log', LOG],
562
649
  ['harness', HARNESS],
563
650
  ['help', HILFE],
564
651
  ]);