hmgr 0.0.29 → 0.0.34

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.
@@ -3,33 +3,23 @@ import spawn from 'cross-spawn';
3
3
  import { realpathSync } from 'node:fs';
4
4
  import { readFile, stat } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
- import { ArbeitsverzeichnisOhneCliZugriff, HeimOhneCliZugriff, McpAbweichenderEintrag, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, OrtNichtUnterstuetzt, pluginSchluessel, wslUncZerlegen, } from "../kern/modell.js";
6
+ import { ClaudeCliNichtErreichbar, McpAbweichenderEintrag, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, OrtNichtUnterstuetzt, pluginSchluessel, wslDistroVonOrt, wslUncZerlegen, } from "../kern/modell.js";
7
7
  import { ohneBom } from "./datei-text.js";
8
8
  import { LokalerZugriff } from "./dateisystem.js";
9
9
  import { gitignoreEintragen, istGitRepo } from "./gitignore.js";
10
10
  import { arbeitsverzeichnisVerbinden, heimVerbinden, nurHome } from "./heime.js";
11
11
  import { tiefGleich } from "./vergleich.js";
12
+ import { WslBruecke, wslAusfuehrerMit, wslClaudeAusfuehrer } from "./wsl.js";
12
13
  import { mitZeitlimit } from "./zeitlimit.js";
13
14
  const CLAUDE_VERZEICHNIS = '.claude';
14
15
  const SKILLS_ORDNER = 'skills';
15
- /**
16
- * Die Adresse eines zusätzlichen Homedirs an diesem Ort (E78), sonst `undefined`. Was die
17
- * claude-CLI angeht — MCP-Server, Plugins —, gilt nur für das primäre Homedir: die CLI auf diesem
18
- * Rechner kennt die Konfiguration einer WSL-Distro nicht.
19
- */
20
- function zusaetzlichesHeim(ort) {
21
- return ort.art === 'home' ? ort.heim : undefined;
16
+ /** Der Pfad eines Arbeitsverzeichnisses in der Welt seiner CLI: in WSL der Linux-Pfad (E100). */
17
+ function cliPfad(pfad) {
18
+ return wslUncZerlegen(pfad)?.linuxPfad ?? pfad;
22
19
  }
23
- /**
24
- * MCP und Plugins gehen nur über die claude-CLI dieses Rechners (E61, E78, E96): ein zusätzliches
25
- * Homedir und ein Arbeitsverzeichnis in WSL liegen außerhalb ihrer Konfiguration.
26
- */
27
- function ohneCliZugriffPruefen(harness, ort, was) {
28
- const heim = zusaetzlichesHeim(ort);
29
- if (heim !== undefined)
30
- throw new HeimOhneCliZugriff(harness, heim, was);
31
- if (ort.art === 'arbeitsverzeichnis' && wslUncZerlegen(ort.pfad) !== undefined)
32
- throw new ArbeitsverzeichnisOhneCliZugriff(harness, ort.pfad, was);
20
+ /** Der Ort, unter dem `ClaudeCode` die claude-CLI einer Distro anspricht: ihr Heim. */
21
+ function distroOrt(distro) {
22
+ return { art: 'home', heim: `wsl:${distro}` };
33
23
  }
34
24
  const GITIGNORE_ZEILE = '.claude/';
35
25
  /**
@@ -60,6 +50,21 @@ export const claudeAusfuehrer = (args, cwd) => new Promise((aufloesen) => {
60
50
  kind.on('error', (fehler) => aufloesen({ stdout: fehler.message, code: 1 }));
61
51
  kind.on('close', (code) => aufloesen({ stdout: code === 0 ? stdout : `${stdout}${stderr}`, code: code ?? 1 }));
62
52
  });
53
+ /**
54
+ * Pfadvergleich für E102: abschließende Trenner zählen nicht; bei Windows-Pfaden (Laufwerk oder
55
+ * UNC) sind außerdem `\\` und `/` sowie Groß-/Kleinschreibung gleich.
56
+ */
57
+ export function pfadGleich(a, b) {
58
+ const windows = (p) => /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith('\\\\');
59
+ const norm = (p, win) => {
60
+ let t = win ? p.replaceAll('\\', '/').toLowerCase() : p;
61
+ while (t.length > 1 && t.endsWith('/'))
62
+ t = t.slice(0, -1);
63
+ return t;
64
+ };
65
+ const win = windows(a) || windows(b);
66
+ return norm(a, win) === norm(b, win);
67
+ }
63
68
  export class PluginListeUnbekannteForm extends Error {
64
69
  constructor(text) {
65
70
  super(`claude plugin list --json: ${text}`);
@@ -96,7 +101,13 @@ export function pluginListeLesen(text) {
96
101
  const scope = o['scope'];
97
102
  if (!istScope(scope))
98
103
  throw new PluginListeUnbekannteForm(`„${id}" hat keinen bekannten Scope`);
99
- return { name: id.slice(0, trenner), marketplace: id.slice(trenner + 1), scope };
104
+ const projectPath = o['projectPath'];
105
+ return {
106
+ name: id.slice(0, trenner),
107
+ marketplace: id.slice(trenner + 1),
108
+ scope,
109
+ ...(typeof projectPath === 'string' ? { projectPath } : {}),
110
+ };
100
111
  });
101
112
  }
102
113
  /**
@@ -116,16 +127,25 @@ export function marketplaceListeLesen(text) {
116
127
  /**
117
128
  * Plugins über `claude plugin …`. Scope ist immer `local` mit dem Arbeitsverzeichnis selbst als
118
129
  * cwd (E27). Nur das Home des Nutzers läuft als `--scope user` ohne cwd.
130
+ *
131
+ * Welche claude-CLI, entscheidet der Ort (E101, derselbe Weg wie MCP seit E100): ein WSL-Heim und
132
+ * ein Arbeitsverzeichnis unter `\\wsl$` gehen an die CLI der Distro, das cwd ist dort der
133
+ * Linux-Pfad. Jede Installation führt ihre eigene Marketplace-Liste, deshalb wirken auch die
134
+ * Marketplace-Operationen in der Welt des genannten Orts. Vor jedem Aufruf prüft die Fähigkeit, ob
135
+ * dort eine CLI antwortet, sonst `ClaudeCliNichtErreichbar`.
119
136
  */
120
137
  class ClaudePlugins {
121
138
  harness;
122
- ausfuehrer;
123
- constructor(harness, ausfuehrer) {
139
+ constructor(harness) {
124
140
  this.harness = harness;
125
- this.ausfuehrer = ausfuehrer;
126
141
  }
127
- async rufen(args, cwd) {
128
- const { stdout, code } = await this.ausfuehrer(args, cwd);
142
+ /** Die claude-CLI des Orts, nachdem feststeht, dass sie antwortet. */
143
+ async cli(ort) {
144
+ await this.harness.cliPruefen(ort);
145
+ return this.harness.ausfuehrerFuer(ort);
146
+ }
147
+ async rufen(ort, args, cwd) {
148
+ const { stdout, code } = await (await this.cli(ort))(args, cwd);
129
149
  if (code !== 0)
130
150
  throw new Error(`claude ${args.join(' ')} scheiterte (Code ${code}): ${stdout.trim()}`);
131
151
  return stdout;
@@ -134,48 +154,80 @@ class ClaudePlugins {
134
154
  * Scope und cwd eines Ortes (E27). Das cwd ist das Arbeitsverzeichnis selbst, nicht sein
135
155
  * Zielverzeichnis: außerhalb eines Git-Repos legt Claude Code die `settings.local.json` im
136
156
  * Startverzeichnis ab, mit dem Zielverzeichnis als cwd landete sie unter `.claude/skills/`.
157
+ * In WSL steht das cwd als Linux-Pfad da (E101).
137
158
  */
138
159
  scope(ziel) {
139
- ohneCliZugriffPruefen(this.harness.name, ziel.ort, 'Plugins');
140
160
  if (ziel.ort.art === 'home')
141
161
  return { scope: 'user' };
142
162
  if (ziel.ort.art === 'arbeitsverzeichnis')
143
- return { scope: 'local', cwd: ziel.ort.pfad };
163
+ return { scope: 'local', cwd: cliPfad(ziel.ort.pfad) };
144
164
  throw new OrtNichtUnterstuetzt(this.harness.name, ziel.ort.art);
145
165
  }
146
- async marketplaceBekannt(name) {
147
- const { stdout, code } = await this.ausfuehrer(['plugin', 'marketplace', 'list']);
166
+ async marketplaceBekannt(name, ort) {
167
+ const { stdout, code } = await (await this.cli(ort))(['plugin', 'marketplace', 'list']);
148
168
  if (code !== 0)
149
169
  return false;
150
170
  return marketplaceListeLesen(stdout).includes(name);
151
171
  }
152
- async marketplaceHinzufuegen(_name, url) {
153
- await this.rufen(['plugin', 'marketplace', 'add', url]);
172
+ /**
173
+ * Eine lokale Quelle steht mit ihrem Windows-Pfad da; die CLI einer Distro braucht ihn in deren
174
+ * Schreibweise (E101): `C:\…` unter der Automount-Wurzel, `\\wsl$\<dieselbe Distro>\…` als
175
+ * Linux-Pfad. URLs und alles andere gehen unverändert durch.
176
+ */
177
+ async marketplaceHinzufuegen(_name, url, ort) {
178
+ await this.rufen(ort, ['plugin', 'marketplace', 'add', await this.urlInWelt(url, ort)]);
179
+ }
180
+ async urlInWelt(url, ort) {
181
+ const distro = wslDistroVonOrt(ort);
182
+ if (distro === undefined)
183
+ return url;
184
+ const unc = wslUncZerlegen(url);
185
+ if (unc !== undefined)
186
+ return unc.distro.toLowerCase() === distro.toLowerCase() ? unc.linuxPfad : url;
187
+ if (/^[a-zA-Z]:[\\/]/.test(url))
188
+ return await this.harness.bruecke(distro).vomHost(url);
189
+ return url;
154
190
  }
155
191
  async installieren(plugin, marketplace, ziel) {
156
192
  const { scope, cwd } = this.scope(ziel);
157
- await this.rufen(['plugin', 'install', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
193
+ await this.rufen(ziel.ort, ['plugin', 'install', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
158
194
  }
159
195
  async entfernen(plugin, marketplace, ziel) {
160
196
  const { scope, cwd } = this.scope(ziel);
161
- await this.rufen(['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
197
+ await this.rufen(ziel.ort, ['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
162
198
  }
163
199
  /** Ohne Namen aktualisiert die CLI alle Marketplaces. Je Plugin verlangt sie einen Namen, das übernimmt hmgr nicht (E14). */
164
- async aktualisieren() {
165
- await this.rufen(['plugin', 'marketplace', 'update']);
200
+ async aktualisieren(ort) {
201
+ await this.rufen(ort, ['plugin', 'marketplace', 'update']);
202
+ }
203
+ async welten() {
204
+ return await this.harness.erreichbareWelten();
166
205
  }
167
206
  /**
168
207
  * Die Liste gilt nur für das cwd des Aufrufs und enthält dort alle Scopes. Zum Ort gehören im
169
208
  * Home die Plugins mit Scope `user`, im Arbeitsverzeichnis die mit `local` und `project`.
209
+ * E102: Die CLI listet die local-Einträge aller Projekte, je mit `projectPath`; am
210
+ * Arbeitsverzeichnis zählen nur die eigenen (ohne `projectPath` wie bisher). Verglichen wird mit
211
+ * dem CLI-Pfad (in WSL der Linux-Pfad) und lokal zusätzlich mit der aufgelösten Form, weil
212
+ * Claude Code den cwd auflöst.
170
213
  */
171
214
  async installierte(ziel) {
172
215
  const { cwd } = this.scope(ziel);
173
- const { stdout, code } = await this.ausfuehrer(['plugin', 'list', '--json'], cwd);
216
+ const { stdout, code } = await (await this.cli(ziel.ort))(['plugin', 'list', '--json'], cwd);
174
217
  if (code !== 0)
175
218
  return [];
176
219
  const erlaubt = ziel.ort.art === 'home' ? ['user'] : ['local', 'project'];
220
+ const eigene = [];
221
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
222
+ eigene.push(cwd ?? ziel.ort.pfad);
223
+ if (wslUncZerlegen(ziel.ort.pfad) === undefined)
224
+ eigene.push(aufgeloest(ziel.ort.pfad));
225
+ }
177
226
  return pluginListeLesen(stdout)
178
227
  .filter((eintrag) => erlaubt.includes(eintrag.scope))
228
+ .filter((eintrag) => ziel.ort.art !== 'arbeitsverzeichnis' ||
229
+ eintrag.projectPath === undefined ||
230
+ eigene.some((pfad) => pfadGleich(pfad, eintrag.projectPath)))
179
231
  .map((eintrag) => pluginSchluessel(eintrag));
180
232
  }
181
233
  }
@@ -222,8 +274,12 @@ function aufgeloest(pfad) {
222
274
  * Windows angelegte Datei leer.
223
275
  */
224
276
  async function projektMcpJsonLesen(verzeichnis) {
277
+ const text = await readFile(path.join(verzeichnis, '.mcp.json'), 'utf8').catch(() => undefined);
278
+ return text === undefined ? {} : mcpServersAusText(text);
279
+ }
280
+ /** `mcpServers` aus dem Text einer `.mcp.json`; kaputtes JSON heißt nichts, eine BOM stört nicht. */
281
+ function mcpServersAusText(text) {
225
282
  try {
226
- const text = await readFile(path.join(verzeichnis, '.mcp.json'), 'utf8');
227
283
  return mcpServersVon(JSON.parse(ohneBom(text)));
228
284
  }
229
285
  catch {
@@ -239,17 +295,43 @@ async function projektMcpJsonLesen(verzeichnis) {
239
295
  * geschrieben wird sie einzig von `geteiltenUebernehmen` auf ausdrücklichen Auftrag (E64). Format
240
296
  * und Verhalten der CLI stehen in HARNESS-FAKTEN.md, geprüft an Claude Code 2.1.261 gegen ein
241
297
  * eigenes CLAUDE_CONFIG_DIR, nie an einer echten Nutzerdatei.
298
+ *
299
+ * Welche CLI und welche Dateien, entscheidet der Ort (E100): ein WSL-Heim und ein Arbeitsverzeichnis
300
+ * unter `\\wsl$` gehen an die claude-CLI der Distro (cwd als Linux-Pfad), und gelesen werden
301
+ * `~/.claude.json` und `.mcp.json` der Distro über die Bridge — die Windows-Dateien wären die
302
+ * falschen. Vor jedem schreibenden Aufruf prüft die Fähigkeit, ob am Ort eine CLI antwortet;
303
+ * sonst hielte das idempotente Aushängen ein „nicht gefunden" der fehlenden CLI für Erfolg.
242
304
  */
243
305
  class ClaudeMcp {
244
306
  harness;
245
- ausfuehrer;
246
307
  /** Das primäre Homedir, jedes Mal frisch: es lässt sich zur Laufzeit umstellen (E78, F89). */
247
308
  home;
248
- constructor(harness, ausfuehrer, home) {
309
+ constructor(harness, home) {
249
310
  this.harness = harness;
250
- this.ausfuehrer = ausfuehrer;
251
311
  this.home = home;
252
312
  }
313
+ /** Die claude-CLI des Orts, nachdem feststeht, dass sie antwortet (E100). */
314
+ async cli(ort) {
315
+ await this.harness.cliPruefen(ort);
316
+ return this.harness.ausfuehrerFuer(ort);
317
+ }
318
+ /** Der Pfad eines Arbeitsverzeichnisses in der Welt seiner CLI: in WSL der Linux-Pfad (E100). */
319
+ cliPfad(pfad) {
320
+ return cliPfad(pfad);
321
+ }
322
+ /** Wo die geteilte `.mcp.json` eines Arbeitsverzeichnisses liegt, in WSL als Linux-Pfad (E100). */
323
+ mcpJsonPfad(pfad) {
324
+ const wsl = wslUncZerlegen(pfad);
325
+ return wsl === undefined ? path.join(pfad, '.mcp.json') : path.posix.join(wsl.linuxPfad, '.mcp.json');
326
+ }
327
+ /** Die geteilte `.mcp.json` eines Arbeitsverzeichnisses, in WSL über die Bridge gelesen (E100). */
328
+ async projektMcpJson(pfad) {
329
+ const wsl = wslUncZerlegen(pfad);
330
+ if (wsl === undefined)
331
+ return await projektMcpJsonLesen(pfad);
332
+ const text = await this.harness.bruecke(wsl.distro).dateiLesen(this.mcpJsonPfad(pfad)).catch(() => undefined);
333
+ return text === undefined ? {} : mcpServersAusText(text);
334
+ }
253
335
  anGitWurzelGebunden = true;
254
336
  ebenen() {
255
337
  return ['home', 'arbeitsverzeichnis'];
@@ -257,12 +339,12 @@ class ClaudeMcp {
257
339
  hinweisNachAenderung() {
258
340
  return 'Claude Code übernimmt die Änderung mit der nächsten Sitzung.';
259
341
  }
342
+ /** Scope und cwd eines Ortes; das cwd steht in der Welt der zuständigen CLI (E100). */
260
343
  scope(ort) {
261
- ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
262
344
  if (ort.art === 'home')
263
345
  return { scope: 'user' };
264
346
  if (ort.art === 'arbeitsverzeichnis')
265
- return { scope: 'local', cwd: ort.pfad };
347
+ return { scope: 'local', cwd: this.cliPfad(ort.pfad) };
266
348
  throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
267
349
  }
268
350
  /**
@@ -273,8 +355,9 @@ class ClaudeMcp {
273
355
  */
274
356
  async einhaengen(name, konfig, ort) {
275
357
  const { scope, cwd } = this.scope(ort);
358
+ const cli = await this.cli(ort);
276
359
  const json = claudeMcpJson(konfig);
277
- const { stdout, code } = await this.ausfuehrer(['mcp', 'add-json', name, json, '--scope', scope], cwd);
360
+ const { stdout, code } = await cli(['mcp', 'add-json', name, json, '--scope', scope], cwd);
278
361
  if (code === 0)
279
362
  return;
280
363
  const bestehend = await this.eintrag(name, ort);
@@ -294,15 +377,16 @@ class ClaudeMcp {
294
377
  */
295
378
  async aushaengen(name, ort) {
296
379
  const { scope, cwd } = this.scope(ort);
297
- const { stdout, code } = await this.ausfuehrer(['mcp', 'remove', name, '--scope', scope], cwd);
380
+ const cli = await this.cli(ort);
381
+ const { stdout, code } = await cli(['mcp', 'remove', name, '--scope', scope], cwd);
298
382
  if (code === 0)
299
383
  return;
300
384
  if ((await this.eintrag(name, ort)) !== undefined)
301
385
  throw new Error(`claude mcp remove ${name} scheiterte (Code ${code}): ${stdout.trim()}`);
302
386
  if (ort.art === 'arbeitsverzeichnis') {
303
- const geteilt = await projektMcpJsonLesen(ort.pfad);
387
+ const geteilt = await this.projektMcpJson(ort.pfad);
304
388
  if (name in geteilt)
305
- throw new McpNurGeteilterFundort({ harness: this.harness.name, ort }, name, path.join(ort.pfad, '.mcp.json'));
389
+ throw new McpNurGeteilterFundort({ harness: this.harness.name, ort }, name, this.mcpJsonPfad(ort.pfad));
306
390
  }
307
391
  }
308
392
  async eingehaengte(ort) {
@@ -310,16 +394,14 @@ class ClaudeMcp {
310
394
  }
311
395
  /**
312
396
  * Die geteilte `.mcp.json` eines Arbeitsverzeichnisses (E61-Nachtrag): im Alltag nur gelesen,
313
- * weder beim Ein- noch beim Aushängen angefasst. Home kennt keine solche Datei.
397
+ * weder beim Ein- noch beim Aushängen angefasst. Home kennt keine solche Datei. In WSL liegt sie
398
+ * im Linux-Dateisystem und wird über die Bridge gelesen, der Pfad steht als Linux-Pfad da (E100).
314
399
  */
315
400
  async geteilteFundorte(ort) {
316
401
  if (ort.art !== 'arbeitsverzeichnis')
317
402
  return [];
318
- // In WSL gibt es hier nichts zu übernehmen (E96): die MCP-Verwaltung ist dort gesperrt.
319
- if (wslUncZerlegen(ort.pfad) !== undefined)
320
- return [];
321
- const datei = path.join(ort.pfad, '.mcp.json');
322
- const server = await projektMcpJsonLesen(ort.pfad);
403
+ const datei = this.mcpJsonPfad(ort.pfad);
404
+ const server = await this.projektMcpJson(ort.pfad);
323
405
  return Object.keys(server).map((name) => ({ name, datei, scope: 'project' }));
324
406
  }
325
407
  /**
@@ -341,12 +423,13 @@ class ClaudeMcp {
341
423
  const ziel = { harness: this.harness.name, ort };
342
424
  if (ort.art !== 'arbeitsverzeichnis')
343
425
  throw new McpUebernahmeNichtMoeglich(ziel);
344
- ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
345
- const datei = path.join(ort.pfad, '.mcp.json');
346
- const block = (await projektMcpJsonLesen(ort.pfad))[name];
426
+ const datei = this.mcpJsonPfad(ort.pfad);
427
+ const block = (await this.projektMcpJson(ort.pfad))[name];
347
428
  if (block === undefined)
348
429
  throw new McpNichtInGeteilterDatei(ziel, name, datei);
349
- const hin = await this.ausfuehrer(['mcp', 'add-json', name, JSON.stringify(block), '--scope', 'local'], ort.pfad);
430
+ const cli = await this.cli(ort);
431
+ const cwd = this.cliPfad(ort.pfad);
432
+ const hin = await cli(['mcp', 'add-json', name, JSON.stringify(block), '--scope', 'local'], cwd);
350
433
  if (hin.code !== 0) {
351
434
  // Der einzige harmlose Fehlschlag: derselbe Block liegt schon im privaten Scope, etwa weil ein
352
435
  // früherer Lauf zwischen den beiden Schritten abgebrochen ist. Ein abweichender Block wird
@@ -357,10 +440,10 @@ class ClaudeMcp {
357
440
  if (!tiefGleich(bestehend, block))
358
441
  throw new McpAbweichenderEintrag(ziel, name);
359
442
  }
360
- const weg = await this.ausfuehrer(['mcp', 'remove', name, '--scope', 'project'], ort.pfad);
443
+ const weg = await cli(['mcp', 'remove', name, '--scope', 'project'], cwd);
361
444
  // Nicht der Code entscheidet, sondern die Datei danach: steht der Name nicht mehr drin, ist die
362
445
  // Übernahme fertig, egal wie die CLI den Fall genannt hat.
363
- if (weg.code !== 0 && name in (await projektMcpJsonLesen(ort.pfad)))
446
+ if (weg.code !== 0 && name in (await this.projektMcpJson(ort.pfad)))
364
447
  throw new McpUebernahmeUnvollstaendig(ziel, name, datei, weg.stdout.trim());
365
448
  return { datei };
366
449
  }
@@ -375,23 +458,35 @@ class ClaudeMcp {
375
458
  * Ordner in einem Git-Repo, ist der Schlüssel die Git-Wurzel, nicht der Ordner (E91, belegt an
376
459
  * 2.1.285): für einen Unterordner liest diese Methode deshalb nichts, und der Kern sperrt dort
377
460
  * die MCP-Operationen, bevor sie den Adapter erreichen.
461
+ *
462
+ * In WSL (E100) ist es die `~/.claude.json` der Distro, über die Bridge gelesen; der Schlüssel ist
463
+ * der Linux-Pfad, zusätzlich in der Distro aufgelöst (`realpath`).
378
464
  */
379
465
  async mcpServersAm(ort) {
380
- ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
381
- const daten = await this.claudeJsonLesen();
466
+ const distro = wslDistroVonOrt(ort);
467
+ const daten = await this.claudeJsonLesen(distro);
382
468
  if (ort.art === 'home')
383
469
  return mcpServersVon(daten);
384
470
  if (ort.art === 'arbeitsverzeichnis') {
385
471
  const projects = objektFeld(daten, 'projects');
386
- const projekt = objektFeld(projects, ort.pfad) ?? objektFeld(projects, aufgeloest(ort.pfad));
387
- return mcpServersVon(projekt);
472
+ const pfad = this.cliPfad(ort.pfad);
473
+ const direkt = objektFeld(projects, pfad);
474
+ if (direkt !== undefined)
475
+ return mcpServersVon(direkt);
476
+ const echt = distro === undefined
477
+ ? aufgeloest(pfad)
478
+ : await this.harness.bruecke(distro).aufgeloest(pfad).catch(() => undefined);
479
+ return mcpServersVon(echt === undefined ? undefined : objektFeld(projects, echt));
388
480
  }
389
481
  throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
390
482
  }
391
- async claudeJsonLesen() {
483
+ /** `~/.claude.json`: unter Windows die im primären Homedir, in WSL die der Distro (E100). */
484
+ async claudeJsonLesen(distro) {
392
485
  try {
393
- const text = await readFile(path.join(this.home(), '.claude.json'), 'utf8');
394
- return JSON.parse(ohneBom(text));
486
+ const text = distro === undefined
487
+ ? await readFile(path.join(this.home(), '.claude.json'), 'utf8')
488
+ : await this.harness.bruecke(distro).claudeJsonLesen();
489
+ return text === undefined ? undefined : JSON.parse(ohneBom(text));
395
490
  }
396
491
  catch {
397
492
  return undefined;
@@ -404,42 +499,123 @@ export class ClaudeCode {
404
499
  ausfuehrer;
405
500
  /** Zugriff je Ort (E78): lokal wie bisher, für ein WSL-Heim die Bridge. */
406
501
  zugriffFuer;
407
- /** Wird von `vorhanden()` gesetzt, sobald die claude-CLI auf dem PATH liegt. */
502
+ /** Die WSL-Anbindung (E100): claude-CLI je Ort, Bridge je Distro, Distros der eingetragenen Orte. */
503
+ wsl;
504
+ /**
505
+ * Plugins (E16) und MCP-Server (E61) setzt `vorhanden()`, sobald irgendwo eine claude-CLI
506
+ * antwortet (E100, E101): unter Windows oder in der Distro eines eingetragenen WSL-Orts. Ob es an
507
+ * einem bestimmten Ort geht, entscheidet der Ort (`cliPruefen`).
508
+ */
408
509
  plugins;
409
- /** Wird von `vorhanden()` gesetzt, sobald die claude-CLI auf dem PATH liegt (E61). */
410
510
  mcp;
411
- geprueft = false;
511
+ /** Ob die claude-CLI antwortet, je Welt einmal je Lauf: Schlüssel '' für Windows, sonst die Distro. */
512
+ erreichbarkeit = new Map();
412
513
  /**
413
514
  * `heime` sagt, wo die Homedirs liegen (E78): das primäre und, seit F87, zusätzliche in einer
414
515
  * WSL-Distro. Ein blanker Pfad steht weiterhin für „nur dieses eine Home", damit Tests und
415
- * Aufrufer ohne Zustand unverändert bleiben.
516
+ * Aufrufer ohne Zustand unverändert bleiben. `wsl` verdrahtet `bauen` (E100); ohne Angabe gehen
517
+ * die Aufrufe an das echte wsl.exe, und als WSL-Orte zählen nur die zusätzlichen Heime.
416
518
  */
417
- constructor(heime, ausfuehrer = claudeAusfuehrer, zugriffFuer) {
519
+ constructor(heime, ausfuehrer = claudeAusfuehrer, zugriffFuer, wsl) {
418
520
  this.heime = typeof heime === 'string' ? nurHome(heime) : heime;
419
521
  this.ausfuehrer = ausfuehrer;
420
522
  const lokal = new LokalerZugriff();
421
523
  this.zugriffFuer = zugriffFuer ?? (() => lokal);
524
+ const bruecken = new Map();
525
+ const langeBridge = wslAusfuehrerMit(120_000);
526
+ this.wsl = {
527
+ ausfuehrerFuer: wsl?.ausfuehrerFuer ??
528
+ ((ort) => {
529
+ const distro = wslDistroVonOrt(ort);
530
+ return distro === undefined ? this.ausfuehrer : wslClaudeAusfuehrer(distro, langeBridge);
531
+ }),
532
+ bruecke: wsl?.bruecke ??
533
+ ((distro) => {
534
+ let b = bruecken.get(distro);
535
+ if (b === undefined)
536
+ bruecken.set(distro, (b = new WslBruecke(distro)));
537
+ return b;
538
+ }),
539
+ distros: wsl?.distros ??
540
+ (async () => this.heime
541
+ .zusaetzliche(this.name)
542
+ .map((heim) => wslDistroVonOrt({ art: 'home', heim: heim.adresse }))
543
+ .filter((distro) => distro !== undefined)),
544
+ };
545
+ }
546
+ /** Die claude-CLI, die für diesen Ort zuständig ist (E100): die des Rechners oder die der Distro. */
547
+ ausfuehrerFuer(ort) {
548
+ return this.wsl.ausfuehrerFuer(ort);
549
+ }
550
+ /** Die Bridge in eine Distro, für das Lesen ihrer Dateien (E100). */
551
+ bruecke(distro) {
552
+ return this.wsl.bruecke(distro);
553
+ }
554
+ /** Ob an diesem Ort eine claude-CLI antwortet (`claude --version`), je Welt einmal je Lauf. */
555
+ async erreichbar(ort) {
556
+ const schluessel = wslDistroVonOrt(ort) ?? '';
557
+ let versprechen = this.erreichbarkeit.get(schluessel);
558
+ if (versprechen === undefined) {
559
+ const cli = schluessel === '' ? this.ausfuehrer : this.ausfuehrerFuer(ort);
560
+ versprechen = cli(['--version'])
561
+ .then(({ code }) => code === 0)
562
+ .catch(() => false);
563
+ this.erreichbarkeit.set(schluessel, versprechen);
564
+ }
565
+ return await versprechen;
566
+ }
567
+ /** Wirft `ClaudeCliNichtErreichbar`, wenn an diesem Ort keine claude-CLI antwortet (E100). */
568
+ async cliPruefen(ort) {
569
+ if (!(await this.erreichbar(ort)))
570
+ throw new ClaudeCliNichtErreichbar({ harness: this.name, ort }, wslDistroVonOrt(ort));
422
571
  }
423
572
  /**
424
573
  * Claude Code liegt auf dem Rechner, wenn es sein Home angelegt hat — oder wenn ein zusätzliches
425
574
  * Homedir eingetragen ist (E78): wer eines eingetragen hat, hat Claude Code dort laufen, und ein
426
- * Windows-Home ohne `.claude` soll die WSL-Ziele nicht mit verschwinden lassen. Die Plugin- und
427
- * MCP-Fähigkeit kommen dazu, sobald `claude --version` durchläuft: ohne CLI gibt es beides nicht.
575
+ * Windows-Home ohne `.claude` soll die WSL-Ziele nicht mit verschwinden lassen.
576
+ *
577
+ * Die Plugin- und die MCP-Fähigkeit (E100, E101) kommen dazu, sobald `claude --version` unter
578
+ * Windows durchläuft — oder, wenn dort keine CLI liegt, sobald sie in der Distro eines
579
+ * eingetragenen WSL-Orts antwortet. Die Distros werden erst dann befragt, nacheinander bis zur
580
+ * ersten Antwort, damit ein Rechner mit Windows-claude keinen wsl.exe-Start zahlt. Jede Welt wird
581
+ * einmal je Lauf geprüft; in der GUI kommt ein später eingetragener WSL-Ort beim nächsten Aufruf
582
+ * dazu, ohne die schon geprüften erneut zu fragen.
428
583
  */
429
584
  async vorhanden() {
430
585
  const primaer = this.heime.primaer(this.name);
431
586
  const da = (await stat(path.join(primaer, CLAUDE_VERZEICHNIS)).catch(() => undefined)) !== undefined ||
432
587
  this.heime.zusaetzliche(this.name).length > 0;
433
- if (!this.geprueft) {
434
- this.geprueft = true;
435
- const { code } = await this.ausfuehrer(['--version']).catch(() => ({ code: 1 }));
436
- if (code === 0) {
437
- this.plugins = new ClaudePlugins(this, this.ausfuehrer);
438
- this.mcp = new ClaudeMcp(this, this.ausfuehrer, () => this.heime.primaer(this.name));
439
- }
588
+ if (this.mcp === undefined && (await this.irgendwoErreichbar())) {
589
+ this.plugins = new ClaudePlugins(this);
590
+ this.mcp = new ClaudeMcp(this, () => this.heime.primaer(this.name));
440
591
  }
441
592
  return da;
442
593
  }
594
+ /** Antwortet irgendwo eine claude-CLI: unter Windows oder in einer Distro der WSL-Orte (E100). */
595
+ async irgendwoErreichbar() {
596
+ if (await this.erreichbar({ art: 'home' }))
597
+ return true;
598
+ const distros = await this.wsl.distros().catch(() => []);
599
+ for (const distro of new Set(distros))
600
+ if (await this.erreichbar(distroOrt(distro)))
601
+ return true;
602
+ return false;
603
+ }
604
+ /**
605
+ * Je Welt mit antwortender claude-CLI ein Ort, der für sie steht (E101): das Windows-Home und je
606
+ * Distro der eingetragenen WSL-Orte ihr Heim. Anders als `irgendwoErreichbar` fragt es alle
607
+ * Distros, auch wenn Windows schon antwortet — jede Welt hat ihre eigenen Marketplaces.
608
+ */
609
+ async erreichbareWelten() {
610
+ const welten = [];
611
+ if (await this.erreichbar({ art: 'home' }))
612
+ welten.push({ art: 'home' });
613
+ const distros = await this.wsl.distros().catch(() => []);
614
+ for (const distro of new Set(distros))
615
+ if (await this.erreichbar(distroOrt(distro)))
616
+ welten.push(distroOrt(distro));
617
+ return welten;
618
+ }
443
619
  orte() {
444
620
  return ['home', 'arbeitsverzeichnis'];
445
621
  }
@@ -121,10 +121,39 @@ async function dateienAufPlatte(wurzel, unter = '') {
121
121
  }
122
122
  return dateien;
123
123
  }
124
+ /**
125
+ * Wartezeiten zwischen den Versuchen bei belegter git-Sperre (E99): 5 Versuche, zusammen rund 3 s.
126
+ * Tests übergeben kürzere über den Konstruktor.
127
+ */
128
+ export const SPERRE_WARTEZEITEN_MS = [200, 400, 800, 1600];
129
+ /** Meldung, mit der git eine belegte Sperrdatei anzeigt (E99). */
130
+ const SPERRE_BELEGT = /Unable to create '[^']+\.lock': File exists/;
124
131
  export class GitQuellen {
125
132
  wurzel;
126
- constructor(wurzel) {
133
+ sperreWartezeitenMs;
134
+ constructor(wurzel, optionen = {}) {
127
135
  this.wurzel = wurzel;
136
+ this.sperreWartezeitenMs = optionen.sperreWartezeitenMs ?? SPERRE_WARTEZEITEN_MS;
137
+ }
138
+ /**
139
+ * Wiederholt einen git-Aufruf, wenn eine fremde Sperre ihn blockiert (E99): GUI-Aktionen und der
140
+ * Start-Lauf rufen git parallel auf, und git bricht bei `….lock: File exists` sofort ab. Nur
141
+ * diese Meldung wird wiederholt, jeder andere Fehler gilt sofort. Nach dem letzten Versuch bleibt
142
+ * die bisherige Fehlermeldung. hmgr löscht fremde Sperren nie, auch keine verwaisten.
143
+ */
144
+ async mitSperrWiederholung(aufruf) {
145
+ for (let versuch = 0;; versuch++) {
146
+ try {
147
+ return await aufruf();
148
+ }
149
+ catch (fehler) {
150
+ const wartezeit = this.sperreWartezeitenMs[versuch];
151
+ const meldung = fehler instanceof Error ? fehler.message : String(fehler);
152
+ if (wartezeit === undefined || !SPERRE_BELEGT.test(meldung))
153
+ throw fehler;
154
+ await new Promise((fertig) => setTimeout(fertig, wartezeit));
155
+ }
156
+ }
128
157
  }
129
158
  /**
130
159
  * stdin ist zu und git darf nicht nach Zugangsdaten fragen, sonst hängt ein wartender Aufruf
@@ -132,6 +161,9 @@ export class GitQuellen {
132
161
  * als dort, ein erster Clone über ein langsames Netz braucht Zeit.
133
162
  */
134
163
  async git(args, cwd) {
164
+ return await this.mitSperrWiederholung(() => this.gitEinmal(args, cwd));
165
+ }
166
+ async gitEinmal(args, cwd) {
135
167
  return await new Promise((erfuellen, ablehnen) => {
136
168
  const kind = spawn('git', args, {
137
169
  cwd,
@@ -155,6 +187,9 @@ export class GitQuellen {
155
187
  }
156
188
  /** Wie `git`, aber mit Text auf stdin und Bytes als Ergebnis: `execFile` kennt keine Eingabe. */
157
189
  async gitMitEingabe(args, cwd, eingabe) {
190
+ return await this.mitSperrWiederholung(() => this.gitMitEingabeEinmal(args, cwd, eingabe));
191
+ }
192
+ async gitMitEingabeEinmal(args, cwd, eingabe) {
158
193
  return await new Promise((erfuellen, ablehnen) => {
159
194
  const kind = spawn('git', args, { cwd, timeout: 300_000, killSignal: 'SIGKILL' });
160
195
  const stuecke = [];
@@ -6,20 +6,51 @@ import path from 'node:path';
6
6
  import { WslBrueckeFehlgeschlagen } from "../kern/modell.js";
7
7
  import { mitZeitlimit } from "./zeitlimit.js";
8
8
  /**
9
- * Standard-Ausführer: `wsl.exe` vom PATH, über cross-spawn wie die claude-CLI (E65/B7). stdin ist
10
- * zu, sonst hängt eine Distro, die auf Eingabe wartet; der Timeout (E98, `mitZeitlimit`) fängt alles Übrige. Ein
11
- * Fehlschlag wird zum Code, nicht zur Ausnahme.
9
+ * Ein Ausführer für `wsl.exe` vom PATH, über cross-spawn wie die claude-CLI (E65/B7). stdin ist zu,
10
+ * sonst hängt eine Distro, die auf Eingabe wartet; das Zeitlimit (E98, `mitZeitlimit`) fängt alles
11
+ * Übrige. Ein Fehlschlag wird zum Code, nicht zur Ausnahme.
12
12
  */
13
- export const wslAusfuehrer = (args) => new Promise((aufloesen) => {
14
- const kind = spawn('wsl.exe', args, { stdio: ['ignore', 'pipe', 'pipe'] });
15
- mitZeitlimit(kind, 60_000);
16
- const teile = [];
17
- const fehlerTeile = [];
18
- kind.stdout.on('data', (d) => teile.push(d));
19
- kind.stderr.on('data', (d) => fehlerTeile.push(d));
20
- kind.on('error', (fehler) => aufloesen({ ausgabe: Buffer.from(fehler.message, 'utf8'), code: 1 }));
21
- kind.on('close', (code) => aufloesen({ ausgabe: Buffer.concat(code === 0 ? teile : [...teile, ...fehlerTeile]), code: code ?? 1 }));
22
- });
13
+ export function wslAusfuehrerMit(zeitlimitMs) {
14
+ return (args) => new Promise((aufloesen) => {
15
+ const kind = spawn('wsl.exe', args, { stdio: ['ignore', 'pipe', 'pipe'] });
16
+ mitZeitlimit(kind, zeitlimitMs);
17
+ const teile = [];
18
+ const fehlerTeile = [];
19
+ kind.stdout.on('data', (d) => teile.push(d));
20
+ kind.stderr.on('data', (d) => fehlerTeile.push(d));
21
+ kind.on('error', (fehler) => aufloesen({ ausgabe: Buffer.from(fehler.message, 'utf8'), code: 1 }));
22
+ kind.on('close', (code) => aufloesen({ ausgabe: Buffer.concat(code === 0 ? teile : [...teile, ...fehlerTeile]), code: code ?? 1 }));
23
+ });
24
+ }
25
+ /** Standard-Ausführer der Bridge: 60 s je Aufruf. */
26
+ export const wslAusfuehrer = wslAusfuehrerMit(60_000);
27
+ /**
28
+ * Sucht die claude-CLI in der Distro und ruft sie mit den übrigen Argumenten auf (E100). `sh -lc`
29
+ * liest das Login-Profil, damit `~/.local/bin` des Native-Installers im PATH steht; der Rückfall auf
30
+ * `$HOME/.local/bin/claude` greift, wenn das Profil ihn nicht einträgt. Gemessen in Ubuntu (dash) und
31
+ * Alpine (busybox), HARNESS-FAKTEN „claude-CLI in einer WSL-Distro aufrufen".
32
+ */
33
+ export const CLAUDE_IN_WSL_SKRIPT = 'c=$(command -v claude || echo "$HOME/.local/bin/claude"); exec "$c" "$@"';
34
+ /**
35
+ * Die Argumente an wsl.exe für ein Programm in der Distro (E100). Nur `--exec` reicht jedes Argument
36
+ * unverändert durch; ohne (und mit `--`) setzt wsl.exe die Zeile zusammen und lässt sie von der
37
+ * Distro-Shell neu zerlegen, Positionsargumente gingen verloren (gemessen).
38
+ */
39
+ export function wslExecArgs(distro, programm, cwd) {
40
+ return ['-d', distro, ...(cwd === undefined ? [] : ['--cd', cwd]), '--exec', ...programm];
41
+ }
42
+ /**
43
+ * Die claude-CLI einer Distro als Ausführer (E100), in derselben Form wie der Windows-Ausführer:
44
+ * `cwd` ist hier ein Linux-Pfad der Distro und geht als `--cd` an wsl.exe. Ausgabe UTF-8 (Ausgabe
45
+ * eines Linux-Programms über die Bridge, gemessen). Liegt keine claude in der Distro, endet das
46
+ * Skript mit Code 127.
47
+ */
48
+ export function wslClaudeAusfuehrer(distro, ausfuehren) {
49
+ return async (args, cwd) => {
50
+ const { ausgabe, code } = await ausfuehren(wslExecArgs(distro, ['sh', '-lc', CLAUDE_IN_WSL_SKRIPT, 'claude', ...args], cwd));
51
+ return { stdout: ausgabe.toString('utf8'), code };
52
+ };
53
+ }
23
54
  /**
24
55
  * Eine wsl.exe-eigene Ausgabe als Text (E78, gemessen): UTF-16LE, mit BOM. Node kennt die Kodierung
25
56
  * als „utf16le"; die BOM bleibt beim Dekodieren als `\uFEFF` stehen und wird hier entfernt. Ein
@@ -96,6 +127,45 @@ export class WslBruecke {
96
127
  throw new WslBrueckeFehlgeschlagen(this.distro, text);
97
128
  return text;
98
129
  }
130
+ /**
131
+ * Ein Programm in der Distro über `--exec` (E100): Argumente kommen unverändert an, `cwd` ist ein
132
+ * Linux-Pfad. Ausgabe UTF-8; ein Fehlschlag ist kein Fehler, sondern ein Code.
133
+ */
134
+ async exec(programm, cwd) {
135
+ const { ausgabe, code } = await this.ausfuehren(wslExecArgs(this.distro, programm, cwd));
136
+ return { text: ausgabe.toString('utf8'), code };
137
+ }
138
+ /** Eine Datei der Distro als Text; `undefined`, wenn sie fehlt oder die Bridge schweigt. */
139
+ async dateiLesen(pfad) {
140
+ const { text, code } = await this.exec(['cat', pfad]);
141
+ return code === 0 ? text : undefined;
142
+ }
143
+ /** `~/.claude.json` des Standardbenutzers der Distro (E100); `undefined` ohne Datei. */
144
+ async claudeJsonLesen() {
145
+ const { text, code } = await this.exec(['sh', '-c', 'cat "$HOME/.claude.json"']);
146
+ return code === 0 ? text : undefined;
147
+ }
148
+ /** Der in der Distro aufgelöste Pfad (`realpath`); `undefined`, wenn es ihn nicht gibt. */
149
+ async aufgeloest(pfad) {
150
+ const { text, code } = await this.exec(['realpath', pfad]);
151
+ const echt = text.trim();
152
+ return code === 0 && echt.length > 0 ? echt : undefined;
153
+ }
154
+ /**
155
+ * Die Git-Wurzel, in der ein Linux-Pfad liegt, und der Pfad relativ dazu (E91, E100). Über die
156
+ * Bridge, weil git für Windows ein Repo unter `\\wsl$` als „dubious ownership" ablehnt (gemessen).
157
+ * `--show-prefix` ist an der Wurzel selbst leer; ein Vergleich der Pfade wäre an einem Symlink im
158
+ * Pfad falsch. Kein Repo, kein git in der Distro oder kein Ordner: `null`.
159
+ */
160
+ async gitWurzel(pfad) {
161
+ const { text, code } = await this.exec(['git', 'rev-parse', '--show-toplevel', '--show-prefix'], pfad);
162
+ if (code !== 0)
163
+ return null;
164
+ const [wurzel, prefix] = text.split('\n').map((zeile) => zeile.replace(/\r$/, '').trim());
165
+ if (wurzel === undefined || wurzel.length === 0)
166
+ return null;
167
+ return { wurzel, relativerPfad: (prefix ?? '').replace(/\/+$/, '') };
168
+ }
99
169
  /** Dasselbe, aber ein Fehlschlag ist kein Fehler: `undefined`. Für lesende Abfragen. */
100
170
  async shWeich(skript) {
101
171
  const { ausgabe, code } = await this.ausfuehren(['-d', this.distro, '/bin/sh', '-c', skript]);
package/dist/cli/hilfe.js CHANGED
@@ -106,8 +106,9 @@ Harnesses, die hmgr verwaltet: standardmäßig nur claude-code (E38).
106
106
 
107
107
  Ein Arbeitsverzeichnis unter Windows, das in einer WSL-Distro liegt (\\\\wsl$\\<distro>\\… oder
108
108
  \\\\wsl.localhost\\<distro>\\…, E96), bedient hmgr über die Bridge dieser Distro, wie ein Homedir in
109
- WSL: der Link liegt im Linux-Dateisystem und zeigt auf den Windows-Checkout (/mnt/c). MCP-Server und
110
- Plugins gibt es dort nicht, die claude-CLI dieses Rechners schreibt nur in die Windows-Konfiguration.
109
+ WSL: der Link liegt im Linux-Dateisystem und zeigt auf den Windows-Checkout (/mnt/c). MCP-Server
110
+ hängt hmgr dort über die claude-CLI der Distro ein (hmgr help mcp, E100). Plugins gibt es dort noch
111
+ nicht, die claude-CLI dieses Rechners schreibt sie nur in die Windows-Konfiguration.
111
112
 
112
113
  Schalter:
113
114
  --composer <pfad> nur bei add: composer.json wählen, wenn unter dem Verzeichnis mehrere in
@@ -228,11 +229,21 @@ Das Schreiben in einen Zeiger ist gesperrt, bis das Composer-Thema entschieden i
228
229
  Arbeitsverzeichnis mit Zeiger brechen add und remove ohne --local ab, nichts wird installiert,
229
230
  verlinkt oder entfernt.
230
231
 
232
+ Unter Windows gehen Plugins auch in WSL (E101): im Homedir einer Distro (--ort wsl:<distro>,
233
+ eingetragen mit hmgr harness home claude-code add wsl:<distro>) und in einem Arbeitsverzeichnis
234
+ unter \\\\wsl$\\<distro>\\… oder \\\\wsl.localhost\\<distro>\\…. hmgr ruft dann die claude-CLI der Distro
235
+ über wsl.exe auf, wie bei MCP-Servern. Jede Claude-Installation führt ihre eigene Marketplace-Liste:
236
+ hmgr trägt den Marketplace in der Distro ein, und hmgr update zieht die Marketplaces unter Windows
237
+ und in jeder Distro der eingetragenen WSL-Orte nach, sofern dort eine claude antwortet. Der
238
+ Marketplace wird in der Distro geklont: eine SSH-URL oder ein privates HTTPS-Repo braucht dort
239
+ eigene Schlüssel bzw. Zugangsdaten, die von Windows gelten nicht. Welche claude zuständig ist,
240
+ entscheidet der Ort: liegt sie nur in WSL, gehen das Windows-Home und Windows-Arbeitsverzeichnisse
241
+ nicht, und umgekehrt.
242
+
231
243
  Schalter (add und remove):
232
244
  --harness <name> claude-code, hermes oder agents, Standard claude-code
233
- --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes.
234
- Ein zusätzliches Homedir (wsl:<distro>) kennt die claude-CLI nicht, dort
235
- gibt es keine Plugins (E78)
245
+ --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
246
+ kennt nur hermes
236
247
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
237
248
  --local Plugin nicht in den Zeiger schreiben
238
249
  --quelle <teil> nur bei add: Quelle eingrenzen, wenn der Name in mehreren Marketplaces steht
@@ -240,6 +251,7 @@ Schalter (add und remove):
240
251
  Beispiele:
241
252
  hmgr plugin list
242
253
  hmgr plugin add superpowers --ort dir:.
254
+ hmgr plugin add superpowers --ort wsl:Ubuntu
243
255
  hmgr plugin remove superpowers
244
256
 
245
257
  Häufige Fälle:
@@ -394,6 +406,17 @@ mcp remove und mcp migrate: die Meldung nennt den Wurzelordner, dort gehören di
394
406
  noch kein Arbeitsverzeichnis: hmgr dir add <wurzel>). Die Wurzel selbst und Ordner ohne Repo gehen
395
407
  wie gewohnt.
396
408
 
409
+ Unter Windows geht MCP auch in WSL (E100): im Homedir einer Distro (--ort wsl:<distro>, eingetragen
410
+ mit hmgr harness home claude-code add wsl:<distro>) und in einem Arbeitsverzeichnis unter
411
+ \\\\wsl$\\<distro>\\… oder \\\\wsl.localhost\\<distro>\\…. hmgr ruft dann die claude-CLI der Distro über
412
+ wsl.exe auf und liest deren ~/.claude.json, nicht die von Windows; gesucht wird claude in einer
413
+ Login-Shell der Distro (command -v claude), sonst unter ~/.local/bin/claude. Die .mcp.json eines
414
+ solchen Projekts nennt hmgr mit ihrem Linux-Pfad. Die Git-Wurzel (siehe oben) ermittelt git in der
415
+ Distro, die Meldung nennt sie als \\\\wsl$-Pfad. Welche claude
416
+ zuständig ist, entscheidet der Ort: liegt sie nur in WSL, gehen das Windows-Home und
417
+ Windows-Arbeitsverzeichnisse nicht, und umgekehrt. Plugins gehen in WSL auf demselben Weg (E101,
418
+ hmgr help plugin).
419
+
397
420
  mcp migrate ist der einzige Weg, auf dem hmgr die geteilte .mcp.json anfasst (E64): der Eintrag
398
421
  wandert 1:1 aus der Datei in deinen privaten Scope am selben Ort und wird dort gelöscht. Auch ein
399
422
  Eintrag, der nicht im Katalog steht, lässt sich so übernehmen — der Konfigblock kommt aus der Datei,
@@ -411,11 +434,13 @@ und, bei einem Hermes-Ziel, zusätzlich gegen <hermes-home>/.env.
411
434
 
412
435
  Schalter (add, remove und migrate):
413
436
  --harness <name> claude-code oder hermes, Standard claude-code
414
- --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home
437
+ --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home
415
438
 
416
439
  Beispiele:
417
440
  hmgr mcp list
418
441
  hmgr mcp add teamwork --ort dir:.
442
+ hmgr mcp add teamwork --ort wsl:Ubuntu
443
+ hmgr mcp add teamwork --ort dir:\\\\wsl$\\Ubuntu\\home\\kollege\\proj
419
444
  hmgr mcp add context7 --harness hermes --ort profil:researcher
420
445
  hmgr mcp remove teamwork --ort dir:.
421
446
  hmgr mcp migrate kollegen-tool --ort dir:.
@@ -431,6 +456,9 @@ Häufige Fälle:
431
456
  den Eintrag in den privaten Scope und löscht ihn aus der Datei.
432
457
  „gibt es keine geteilte Konfigurationsdatei": mcp migrate geht nur mit --harness claude-code und
433
458
  --ort dir:<pfad>.
459
+ „ist keine claude-CLI erreichbar": an diesem Ort antwortet keine claude. In WSL Claude Code in der
460
+ Distro installieren und mit wsl -d <distro> --exec sh -lc "command -v claude" prüfen; unter
461
+ Windows muss claude im PATH liegen.
434
462
 
435
463
  Siehe auch: hmgr help harness, hmgr help status`;
436
464
  const SYNC = `hmgr sync [pfad]
@@ -625,8 +653,9 @@ dort nutzt. Das Home wird einmal ermittelt (wsl -d <distro> /bin/sh -c 'echo $HO
625
653
  gespeichert. Jedes Homedir ist danach ein eigenständiges Ziel: es steht als eigene Zeile im status und
626
654
  wird mit --ort wsl:<distro> angesprochen. Gespiegelt wird nichts, jedes Ziel wird einzeln verlinkt.
627
655
  Links dort legt hmgr über wsl.exe an; das Linkziel bleibt der vorhandene Windows-Checkout, übersetzt
628
- nach /mnt/c. MCP-Server und Plugins gibt es dort nicht: die claude-CLI dieses Rechners bedient nur das
629
- primäre Homedir. remove lässt hmgr das Homedir vergessen, die Links dort bleiben liegen.
656
+ nach /mnt/c. MCP-Server hängt hmgr dort über die claude-CLI der Distro ein (hmgr help mcp, E100).
657
+ Plugins gibt es dort noch nicht: die claude-CLI dieses Rechners bedient sie nur im primären Homedir.
658
+ remove lässt hmgr das Homedir vergessen, die Links dort bleiben liegen.
630
659
 
631
660
  Ein Arbeitsverzeichnis unter \\\\wsl$\\<distro>\\… bedient hmgr ebenso über die Distro (hmgr help dir, E96).
632
661
 
package/dist/cli/main.js CHANGED
@@ -4,15 +4,15 @@ 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, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, ArbeitsverzeichnisOhneCliZugriff, HeimOhneCliZugriff, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
7
+ import { Anwendungsfaelle, pluginWeltText, } from "../kern/anwendungsfaelle.js";
8
+ import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, ClaudeCliNichtErreichbar, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
9
9
  import { Agents } from "../adapter/agents.js";
10
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
13
  import { DateisystemZiele, LokalerZugriff } from "../adapter/dateisystem.js";
14
14
  import { ZustandHeime } from "../adapter/heime.js";
15
- import { WslBruecke, WslZugriff, wslAusfuehrer, wslDistros } from "../adapter/wsl.js";
15
+ import { WslBruecke, WslZugriff, wslAusfuehrer, wslAusfuehrerMit, wslClaudeAusfuehrer, wslDistros, } from "../adapter/wsl.js";
16
16
  import { GitQuellen } from "../adapter/git.js";
17
17
  import { DateiMcpKatalog } from "../adapter/mcp-katalog.js";
18
18
  import { cliUpdateHinweis } from "../adapter/npm-registry.js";
@@ -58,7 +58,33 @@ export function bauen(umgebung, optionen = {}) {
58
58
  const distro = wslDistroVonOrt(ort);
59
59
  return distro === undefined ? lokal : new WslZugriff(bruecke(distro));
60
60
  };
61
- const claude = new ClaudeCode(heime, optionen.claudeAusfuehrer ?? claudeAusfuehrer, zugriffFuerOrt);
61
+ // Die claude-CLI je Ort (E100): für ein WSL-Heim und ein Arbeitsverzeichnis unter \\wsl$ die der
62
+ // Distro, über `wsl.exe --exec` mit eigenem Zeitlimit von 120 s wie unter Windows — nicht die 60 s
63
+ // der Bridge, `mcp add-json` startet in der Distro eine ganze Node-Laufzeit.
64
+ const windowsClaude = optionen.claudeAusfuehrer ?? claudeAusfuehrer;
65
+ const wslClaudeAusfuehren = optionen.wslAusfuehrer ?? wslAusfuehrerMit(120_000);
66
+ const claudeFuerOrt = (ort) => {
67
+ const distro = wslDistroVonOrt(ort);
68
+ return distro === undefined ? windowsClaude : wslClaudeAusfuehrer(distro, wslClaudeAusfuehren);
69
+ };
70
+ // Die Distros der eingetragenen WSL-Orte (E100): zusätzliche Heime und Arbeitsverzeichnisse unter
71
+ // \\wsl$. Hier statt im Adapter, weil nur der Zustand die Arbeitsverzeichnisse kennt. Außerhalb von
72
+ // Windows keine: ein von dort übernommener Zustand soll hier kein wsl.exe starten.
73
+ const wslDistrosDerOrte = async () => {
74
+ if (umgebungPort.plattform() !== 'win32')
75
+ return [];
76
+ const distros = heime
77
+ .zusaetzliche('claude-code')
78
+ .map((heim) => wslDistroVonOrt({ art: 'home', heim: heim.adresse }));
79
+ for (const pfad of (await zustand.lesen()).arbeitsverzeichnisse)
80
+ distros.push(wslUncZerlegen(pfad)?.distro);
81
+ return [...new Set(distros.filter((distro) => distro !== undefined))];
82
+ };
83
+ const claude = new ClaudeCode(heime, windowsClaude, zugriffFuerOrt, {
84
+ ausfuehrerFuer: claudeFuerOrt,
85
+ bruecke,
86
+ distros: wslDistrosDerOrte,
87
+ });
62
88
  const ports = {
63
89
  harnesses: [claude, new Hermes(heime, zugriffFuerOrt), new Agents(heime, zugriffFuerOrt)],
64
90
  quellen: new GitQuellen(wurzel),
@@ -72,6 +98,7 @@ export function bauen(umgebung, optionen = {}) {
72
98
  wsl: {
73
99
  distros: async () => await wslDistros(wslAusfuehren),
74
100
  home: async (distro) => await bruecke(distro).home(),
101
+ gitWurzel: async (distro, linuxPfad) => await bruecke(distro).gitWurzel(linuxPfad),
75
102
  },
76
103
  verlauf,
77
104
  };
@@ -729,7 +756,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
729
756
  : updateHinweis !== undefined
730
757
  ? await anwendungsfaelle.quelleZuHinweis(updateHinweis)
731
758
  : undefined;
732
- const { checkouts: liste, bundles, aufraeumen, doctor, } = await anwendungsfaelle.aktualisieren({ ausloeser: 'cli', ...(nurQuelle === undefined ? {} : { quelle: nurQuelle }) });
759
+ const { checkouts: liste, pluginFehler, bundles, aufraeumen, doctor, } = await anwendungsfaelle.aktualisieren({ ausloeser: 'cli', ...(nurQuelle === undefined ? {} : { quelle: nurQuelle }) });
733
760
  if (liste.length === 0)
734
761
  console.log('Keine Checkouts.');
735
762
  for (const { checkout, aktualisierung, geaenderteSkills } of liste) {
@@ -748,6 +775,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
748
775
  else
749
776
  console.log(` fehlgeschlagen ${kopf} ${aktualisierung.fehler}`);
750
777
  }
778
+ // Plugins je Welt (E101): genannt wird nur ein Fehlschlag, eine Änderung lässt sich nicht messen (E80).
779
+ for (const { ziel, fehler } of pluginFehler)
780
+ console.log(` fehlgeschlagen Plugins ${ziel.harness} ${pluginWeltText(ziel.ort)} ${fehler}`);
751
781
  // Bundles im selben Lauf (E69, F64=O1): eine geänderte Definition wird hier nachgezogen.
752
782
  for (const zeile of bundleBericht(bundles))
753
783
  console.log(zeile);
@@ -1152,8 +1182,7 @@ function istModellFehler(fehler) {
1152
1182
  fehler instanceof HeimBereitsVorhanden ||
1153
1183
  fehler instanceof HeimFaehigkeitFehlt ||
1154
1184
  fehler instanceof HeimNurUnterWindows ||
1155
- fehler instanceof HeimOhneCliZugriff ||
1156
- fehler instanceof ArbeitsverzeichnisOhneCliZugriff ||
1185
+ fehler instanceof ClaudeCliNichtErreichbar ||
1157
1186
  fehler instanceof HeimUnbekannt ||
1158
1187
  fehler instanceof WslBrueckeFehlgeschlagen);
1159
1188
  }
@@ -1206,10 +1235,10 @@ function naechsterSchritt(fehler) {
1206
1235
  return `hmgr harness home ${fehler.harness} zeigt die eingetragenen Homedirs.`;
1207
1236
  if (fehler instanceof HeimUnbekannt)
1208
1237
  return `hmgr harness home ${fehler.harness} zeigt die eingetragenen Homedirs, add trägt eines ein.`;
1209
- if (fehler instanceof ArbeitsverzeichnisOhneCliZugriff)
1210
- return 'Skill-Links dort legt hmgr selbst: hmgr link <skill> --ort dir:<pfad>.';
1211
- if (fehler instanceof HeimOhneCliZugriff)
1212
- return 'Skill-Links dort legt hmgr selbst: hmgr link <skill> --harness claude-code --ort <adresse>.';
1238
+ if (fehler instanceof ClaudeCliNichtErreichbar)
1239
+ return fehler.distro === undefined
1240
+ ? 'Claude Code hier installieren, oder an einem Ort arbeiten, an dem claude läuft, etwa --ort wsl:<distro>.'
1241
+ : `Claude Code in der Distro installieren, dann prüfen: wsl -d ${fehler.distro} --exec sh -lc "command -v claude"`;
1213
1242
  if (fehler instanceof WslBrueckeFehlgeschlagen)
1214
1243
  return 'wsl -l -q zeigt die installierten Distros. Startet die Distro nicht, hilft wsl --shutdown und ein erneuter Versuch.';
1215
1244
  if (fehler instanceof QuelleUnbekannt)
@@ -142,7 +142,7 @@
142
142
  <h1>MCP-Server</h1>
143
143
  <!-- Der Pfad steht hier in POSIX-Schreibweise und wird beim Start in die Schreibweise
144
144
  der Plattform gesetzt (E68, ladeUmgebung im Renderer). -->
145
- <p>Dein Katalog aus <span class="mono" id="mcpkatalogpfad">~/.hmgr/mcp.yaml</span>. Häng einen Server dorthin ein, wo du ihn brauchst: in ein Arbeitsverzeichnis, damit er nur dort Kontext kostet, oder global. Den Katalog selbst pflegst du in der Datei, hmgr liest sie nur. Arbeitsverzeichnisse in WSL (\\wsl$\…) gehen dort nicht: die claude-CLI dieses Rechners kennt ihre Konfiguration nicht.</p>
145
+ <p>Dein Katalog aus <span class="mono" id="mcpkatalogpfad">~/.hmgr/mcp.yaml</span>. Häng einen Server dorthin ein, wo du ihn brauchst: in ein Arbeitsverzeichnis, damit er nur dort Kontext kostet, oder global. Den Katalog selbst pflegst du in der Datei, hmgr liest sie nur. In WSL – im Homedir einer Distro und in Arbeitsverzeichnissen unter \\wsl$\… – hängt hmgr den Server über die claude-CLI der Distro ein. Plugins gehen dort auf demselben Weg.</p>
146
146
  </div>
147
147
 
148
148
  <div class="mcpliste" id="mcpliste"></div>
@@ -167,7 +167,7 @@
167
167
  <section id="ansicht-plugins" hidden>
168
168
  <div class="ansichtskopf">
169
169
  <h1>Plugins</h1>
170
- <p>Ein Marketplace ist eine Quelle mit einer <span class="mono">marketplace.json</span>. Die Plugins darin bündeln Skills und können voneinander abhängen. Harnesses ohne Plugin-Konzept bekommen von hmgr stattdessen die flache Skill-Liste verlinkt.</p>
170
+ <p>Ein Marketplace ist eine Quelle mit einer <span class="mono">marketplace.json</span>. Die Plugins darin bündeln Skills und können voneinander abhängen. Harnesses ohne Plugin-Konzept bekommen von hmgr stattdessen die flache Skill-Liste verlinkt. In WSL installiert hmgr Plugins über die claude-CLI der Distro; jede Claude-Installation führt ihre eigene Marketplace-Liste, hmgr trägt den Marketplace dort ein und zieht ihn beim Aktualisieren nach.</p>
171
171
  </div>
172
172
  <div id="pluginliste"></div>
173
173
  </section>
@@ -1,11 +1,19 @@
1
1
  // Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
2
2
  import path from 'node:path';
3
- import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
3
+ import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncBilden, wslUncZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
4
4
  import { mitBegrenzung } from "./begrenzung.js";
5
5
  import { passenderRef } from "./version.js";
6
6
  import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, kannZusaetzlicheHeime, } from "../ports/harness.js";
7
7
  /** So viele `plugin list`-Prozesse laufen im Ist-Zustand höchstens gleichzeitig (E93). */
8
8
  const PLUGIN_ABFRAGEN_GLEICHZEITIG = 6;
9
+ /**
10
+ * Die Welt eines Orts als Text für Bericht und Verlauf (E101), in der Schreibweise von `--ort`:
11
+ * „home" für die Installation dieses Rechners, „wsl:<distro>" für die einer Distro.
12
+ */
13
+ export function pluginWeltText(ort) {
14
+ const distro = wslDistroVonOrt(ort);
15
+ return distro === undefined ? 'home' : `wsl:${distro}`;
16
+ }
9
17
  /**
10
18
  * Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
11
19
  * Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
@@ -357,6 +365,15 @@ export class Anwendungsfaelle {
357
365
  async mcpGitWurzelFuer(faehig, ziel) {
358
366
  if (faehig.anGitWurzelGebunden !== true || ziel.ort.art !== 'arbeitsverzeichnis')
359
367
  return null;
368
+ // In WSL fragt hmgr git in der Distro (E100); die Wurzel kommt in der Schreibweise des
369
+ // gespeicherten Pfads zurück, damit die Meldung einen Pfad nennt, den `hmgr dir add` nimmt.
370
+ const wsl = wslUncZerlegen(ziel.ort.pfad);
371
+ if (wsl !== undefined) {
372
+ const funde = await this.p.wsl.gitWurzel(wsl.distro, wsl.linuxPfad).catch(() => null);
373
+ if (funde === null || funde.relativerPfad === '')
374
+ return null;
375
+ return wslUncBilden(ziel.ort.pfad, funde.wurzel) ?? funde.wurzel;
376
+ }
360
377
  const kopie = await this.p.quellen.arbeitskopie(ziel.ort.pfad).catch(() => null);
361
378
  if (kopie === null || kopie.relativerPfad === '')
362
379
  return null;
@@ -464,18 +481,17 @@ export class Anwendungsfaelle {
464
481
  if (!kannMcp(harness))
465
482
  continue;
466
483
  const ebenen = harness.mcp.ebenen();
467
- // Nur das primäre Homedir (E78): die claude-CLI dieses Rechners bedient die Konfiguration
468
- // einer WSL-Distro nicht, dort gibt es keine MCP-Server zu verwalten.
484
+ // Alle Homedirs samt WSL-Heimen und alle Arbeitsverzeichnisse samt denen unter \\wsl$ (E100):
485
+ // der Adapter spricht dort die claude-CLI der Distro an. Ob an einem Ort eine CLI antwortet,
486
+ // entscheidet er beim Schreiben; die Übersicht liest ohnehin nur.
469
487
  if (ebenen.includes('home'))
470
- ziele.push({ harness: harness.name, ort: { art: 'home' } });
488
+ ziele.push(...this.homeZiele(harness));
471
489
  if (ebenen.includes('profil') && kannProfile(harness))
472
490
  for (const name of await harness.profile())
473
491
  ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
474
- // Arbeitsverzeichnisse in WSL (E96) ebenso nicht: ihre Konfiguration liegt in der Distro.
475
492
  if (ebenen.includes('arbeitsverzeichnis'))
476
493
  for (const pfad of zustand.arbeitsverzeichnisse)
477
- if (wslUncZerlegen(pfad) === undefined)
478
- ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
494
+ ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
479
495
  }
480
496
  return ziele;
481
497
  }
@@ -1526,8 +1542,9 @@ export class Anwendungsfaelle {
1526
1542
  if (!harness.orte().includes(ziel.ort.art))
1527
1543
  throw new OrtNichtUnterstuetzt(harness.name, ziel.ort.art);
1528
1544
  if (kannPlugins(harness)) {
1529
- if (!(await harness.plugins.marketplaceBekannt(fund.plugin.marketplace)))
1530
- await harness.plugins.marketplaceHinzufuegen(fund.plugin.marketplace, marketplaceUrl(fund.quelle));
1545
+ // Die Marketplace-Liste gehört zur Welt des Ziels (E101): ein WSL-Ziel trägt ihn in der Distro ein.
1546
+ if (!(await harness.plugins.marketplaceBekannt(fund.plugin.marketplace, ziel.ort)))
1547
+ await harness.plugins.marketplaceHinzufuegen(fund.plugin.marketplace, marketplaceUrl(fund.quelle), ziel.ort);
1531
1548
  await harness.vorbereiten(ziel.ort);
1532
1549
  await harness.plugins.installieren(fund.plugin.name, fund.plugin.marketplace, ziel);
1533
1550
  return { plugin: fund.plugin, art: 'installiert', skills: [], imZeiger: false };
@@ -1860,13 +1877,26 @@ export class Anwendungsfaelle {
1860
1877
  }
1861
1878
  this.nachgezogen.add(quelleSchluessel(quelle));
1862
1879
  }
1863
- // Nach den Checkouts die Plugins nachziehen (E14).
1880
+ // Nach den Checkouts die Plugins nachziehen (E14), je Welt mit erreichbarer Installation
1881
+ // (E101): Windows und jede Distro der eingetragenen WSL-Orte haben ihre eigenen Marketplaces.
1882
+ // Ein Fehlschlag in einer Welt steht im Ergebnis und bricht den Lauf nicht ab.
1864
1883
  const pluginHarnesses = [];
1884
+ const pluginFehler = [];
1865
1885
  for (const harness of nurSchluessel !== undefined ? [] : await this.vorhandeneHarnesses()) {
1866
1886
  if (!kannPlugins(harness))
1867
1887
  continue;
1868
- await harness.plugins.aktualisieren();
1869
- pluginHarnesses.push(harness.name);
1888
+ let erfolgreich = false;
1889
+ for (const ort of await harness.plugins.welten()) {
1890
+ try {
1891
+ await harness.plugins.aktualisieren(ort);
1892
+ erfolgreich = true;
1893
+ }
1894
+ catch (fehler) {
1895
+ pluginFehler.push({ ziel: { harness: harness.name, ort }, fehler: fehlertext(fehler) });
1896
+ }
1897
+ }
1898
+ if (erfolgreich)
1899
+ pluginHarnesses.push(harness.name);
1870
1900
  }
1871
1901
  // Bundles vor dem Aufräumen (E69, F64=O1): der Abgleich legt und entfernt Links, und erst
1872
1902
  // danach steht fest, in welchen Checkout noch ein Link zeigt. Ein Fehlschlag je Bundle steckt
@@ -1878,15 +1908,16 @@ export class Anwendungsfaelle {
1878
1908
  const doctorErgebnis = nurSchluessel !== undefined
1879
1909
  ? { fehlendeArbeitsverzeichnisse: [], vergessen: [] }
1880
1910
  : await this.doctor({ rueckfragen });
1881
- await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis));
1882
- return { checkouts, pluginHarnesses, bundles, aufraeumen, doctor: doctorErgebnis };
1911
+ await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis));
1912
+ return { checkouts, pluginHarnesses, pluginFehler, bundles, aufraeumen, doctor: doctorErgebnis };
1883
1913
  }
1884
1914
  /**
1885
- * Ereignisse eines Aktualisieren-Laufs für den Verlauf (E80): Plugins bleiben draußen, weil sich
1886
- * eine Änderung dort nicht messen lässt (`claude plugin marketplace update` liefert keine
1887
- * Auskunft) — die bisherige Zeile „Plugins aktualisiert" war deshalb geschönt.
1915
+ * Ereignisse eines Aktualisieren-Laufs für den Verlauf (E80): eine Plugin-Änderung bleibt
1916
+ * draußen, weil sie sich nicht messen lässt (`claude plugin marketplace update` liefert keine
1917
+ * Auskunft) — die bisherige Zeile „Plugins aktualisiert" war deshalb geschönt. Ein Fehlschlag
1918
+ * je Welt (E101) steht dagegen drin.
1888
1919
  */
1889
- aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis) {
1920
+ aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis) {
1890
1921
  const ereignisse = [];
1891
1922
  for (const { checkout, aktualisierung, geaenderteSkills } of checkouts) {
1892
1923
  if (aktualisierung.ergebnis === 'aktualisiert')
@@ -1905,6 +1936,8 @@ export class Anwendungsfaelle {
1905
1936
  fehler: aktualisierung.fehler,
1906
1937
  });
1907
1938
  }
1939
+ for (const { ziel, fehler } of pluginFehler)
1940
+ ereignisse.push({ art: 'fehler', betrifft: `Plugins ${ziel.harness} ${pluginWeltText(ziel.ort)}`, fehler });
1908
1941
  for (const eintrag of bundles) {
1909
1942
  if (eintrag.ergebnis === 'fehlgeschlagen')
1910
1943
  ereignisse.push({ art: 'fehler', betrifft: eintrag.bundle, fehler: eintrag.fehler ?? '' });
@@ -2365,20 +2398,18 @@ export class Anwendungsfaelle {
2365
2398
  }
2366
2399
  // Installierte Plugins je Ziel vorab, mit begrenzter Parallelität (E93): `claude plugin list`
2367
2400
  // ist ein Prozess je Ziel (cwd-gebunden, kein Sammelaufruf), nacheinander dauert das bei vielen
2368
- // Arbeitsverzeichnissen Sekunden. Plugins nur am primären Homedir (E78) und in Arbeitsverzeichnissen
2369
- // außerhalb von WSL (E96): die claude-CLI dieses Rechners kennt die Plugin-Konfiguration einer
2370
- // WSL-Distro nicht, ihre user-Liste gehörte zum Windows-Home.
2401
+ // Arbeitsverzeichnissen Sekunden. WSL-Ziele fragen die claude-CLI ihrer Distro (E101); ein Ort
2402
+ // ohne erreichbare CLI ergibt eine leere Liste.
2371
2403
  const pluginListen = new Map();
2372
2404
  if (mitPlugins) {
2373
2405
  const abfragen = [];
2374
2406
  for (const { harness, ziele } of plan)
2375
2407
  if (kannPlugins(harness))
2376
2408
  for (const ziel of ziele)
2377
- if (wslDistroVonOrt(ziel.ort) === undefined)
2378
- abfragen.push({
2379
- schluessel: zielSchluessel(ziel),
2380
- abruf: () => harness.plugins.installierte(ziel).catch(() => []),
2381
- });
2409
+ abfragen.push({
2410
+ schluessel: zielSchluessel(ziel),
2411
+ abruf: () => harness.plugins.installierte(ziel).catch(() => []),
2412
+ });
2382
2413
  const listen = await mitBegrenzung(PLUGIN_ABFRAGEN_GLEICHZEITIG, abfragen.map((a) => a.abruf));
2383
2414
  abfragen.forEach((a, i) => pluginListen.set(a.schluessel, listen[i]));
2384
2415
  }
@@ -83,6 +83,19 @@ export function wslUncZerlegen(pfad) {
83
83
  const rest = (treffer[2] ?? '').split('\\').filter((teil) => teil.length > 0);
84
84
  return { distro, linuxPfad: `/${rest.join('/')}` };
85
85
  }
86
+ /**
87
+ * Ein Linux-Pfad der Distro in der UNC-Schreibweise eines vorhandenen WSL-Pfads (E100): Präfix und
88
+ * Distro kommen aus `vorlage` (so, wie der Nutzer sie geschrieben hat), der Rest aus `linuxPfad`.
89
+ * So nennt etwa die E91-Meldung eine Git-Wurzel, die sich direkt an `hmgr dir add` geben lässt.
90
+ * Ist `vorlage` kein WSL-UNC-Pfad: undefined.
91
+ */
92
+ export function wslUncBilden(vorlage, linuxPfad) {
93
+ const treffer = /^(\\\\(?:\?\\UNC\\)?(?:wsl\$|wsl\.localhost)\\[^\\]+)/i.exec(vorlage.trim().replaceAll('/', '\\'));
94
+ if (treffer === null)
95
+ return undefined;
96
+ const teile = linuxPfad.split('/').filter((teil) => teil.length > 0);
97
+ return `${treffer[1]}\\${teile.join('\\')}`;
98
+ }
86
99
  /** Die WSL-Distro, über deren Bridge ein Ort bedient wird: ein `wsl:`-Heim (E78) oder ein WSL-UNC-Arbeitsverzeichnis (E96). */
87
100
  export function wslDistroVonOrt(ort) {
88
101
  if (ort.art === 'home')
@@ -146,34 +159,30 @@ export class HeimUnbekannt extends Error {
146
159
  }
147
160
  }
148
161
  /**
149
- * Etwas, das nur am primären Homedir geht, wurde für ein zusätzliches verlangt (E78): die
150
- * claude-CLI dieses Rechners bedient das Windows-Home, nicht das einer WSL-Distro — MCP-Server und
151
- * Plugins lassen sich dort deshalb nicht verwalten. Skill-Links legt hmgr dort selbst, ohne CLI.
162
+ * An einem Ort antwortet keine claude-CLI (E100, E101). MCP-Server und Plugins verwaltet hmgr
163
+ * ausschließlich über die CLI (E16, E61); welche CLI zuständig ist, entscheidet der Ort: unter
164
+ * Windows die vom PATH, in einem WSL-Heim oder WSL-Arbeitsverzeichnis die der Distro. Gibt es nur
165
+ * eine von beiden, gehen die Orte der anderen Welt nicht.
152
166
  */
153
- export class HeimOhneCliZugriff extends Error {
154
- harness;
155
- adresse;
156
- constructor(harness, adresse, was) {
157
- super(`${harness} kann ${was} im Homedir „${adresse}" nicht verwalten: die claude-CLI dieses Rechners bedient nur ` +
158
- 'das primäre Homedir. Skill-Links legt hmgr dort selbst.');
159
- this.harness = harness;
160
- this.adresse = adresse;
161
- }
162
- }
163
- /**
164
- * MCP-Server oder Plugins für ein Arbeitsverzeichnis in WSL verlangt (E96): die claude-CLI dieses
165
- * Rechners schreibt in die Windows-Konfiguration, die das Claude in der Distro nie liest. Skill-Links
166
- * legt hmgr dort selbst, über die Bridge der Distro.
167
- */
168
- export class ArbeitsverzeichnisOhneCliZugriff extends Error {
169
- harness;
170
- pfad;
171
- constructor(harness, pfad, was) {
172
- super(`${harness === 'claude-code' ? 'Claude Code' : harness} kann ${was} im Arbeitsverzeichnis „${pfad}" nicht verwalten: ` +
173
- 'es liegt in WSL, die claude-CLI dieses Rechners schreibt nur in die Windows-Konfiguration. ' +
174
- 'Skill-Links legt hmgr dort selbst.');
175
- this.harness = harness;
176
- this.pfad = pfad;
167
+ export class ClaudeCliNichtErreichbar extends Error {
168
+ ziel;
169
+ distro;
170
+ constructor(ziel, distro) {
171
+ const ort = ziel.ort.art === 'home'
172
+ ? ziel.ort.heim === undefined
173
+ ? 'dem Homedir'
174
+ : `dem Homedir ${ziel.ort.heim}`
175
+ : ziel.ort.art === 'arbeitsverzeichnis'
176
+ ? `dem Arbeitsverzeichnis „${ziel.ort.pfad}"`
177
+ : `dem Profil ${ziel.ort.name}`;
178
+ super(distro === undefined
179
+ ? `In ${ort} ist keine claude-CLI erreichbar: auf diesem Rechner liegt keine claude im PATH ` +
180
+ '(claude --version scheiterte). MCP-Server und Plugins lassen sich dort erst verwalten, wenn Claude Code hier installiert ist.'
181
+ : `In ${ort} ist keine claude-CLI erreichbar: in der WSL-Distro ${distro} fand hmgr keine ` +
182
+ '(gesucht mit command -v claude in einer Login-Shell, danach unter ~/.local/bin/claude). ' +
183
+ 'MCP-Server und Plugins lassen sich dort erst verwalten, wenn Claude Code in der Distro installiert ist.');
184
+ this.ziel = ziel;
185
+ this.distro = distro;
177
186
  }
178
187
  }
179
188
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.29",
3
+ "version": "0.0.34",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",