hmgr 0.0.30 → 0.0.35

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,35 +3,27 @@ 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/';
25
+ /** Wie lange „keine claude-CLI erreichbar" gilt, bevor erneut gefragt wird (E103). */
26
+ const ERREICHBARKEIT_NEGATIV_MS = 60_000;
35
27
  /**
36
28
  * Standard-Ausführer: `claude` vom PATH. Ein Fehlschlag wird zum Code, nicht zur Ausnahme.
37
29
  * stdin ist zu, sonst hängt eine CLI, die auf Eingabe wartet, den ganzen Ablauf; der Timeout
@@ -60,6 +52,21 @@ export const claudeAusfuehrer = (args, cwd) => new Promise((aufloesen) => {
60
52
  kind.on('error', (fehler) => aufloesen({ stdout: fehler.message, code: 1 }));
61
53
  kind.on('close', (code) => aufloesen({ stdout: code === 0 ? stdout : `${stdout}${stderr}`, code: code ?? 1 }));
62
54
  });
55
+ /**
56
+ * Pfadvergleich für E102: abschließende Trenner zählen nicht; bei Windows-Pfaden (Laufwerk oder
57
+ * UNC) sind außerdem `\\` und `/` sowie Groß-/Kleinschreibung gleich.
58
+ */
59
+ export function pfadGleich(a, b) {
60
+ const windows = (p) => /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith('\\\\');
61
+ const norm = (p, win) => {
62
+ let t = win ? p.replaceAll('\\', '/').toLowerCase() : p;
63
+ while (t.length > 1 && t.endsWith('/'))
64
+ t = t.slice(0, -1);
65
+ return t;
66
+ };
67
+ const win = windows(a) || windows(b);
68
+ return norm(a, win) === norm(b, win);
69
+ }
63
70
  export class PluginListeUnbekannteForm extends Error {
64
71
  constructor(text) {
65
72
  super(`claude plugin list --json: ${text}`);
@@ -96,7 +103,13 @@ export function pluginListeLesen(text) {
96
103
  const scope = o['scope'];
97
104
  if (!istScope(scope))
98
105
  throw new PluginListeUnbekannteForm(`„${id}" hat keinen bekannten Scope`);
99
- return { name: id.slice(0, trenner), marketplace: id.slice(trenner + 1), scope };
106
+ const projectPath = o['projectPath'];
107
+ return {
108
+ name: id.slice(0, trenner),
109
+ marketplace: id.slice(trenner + 1),
110
+ scope,
111
+ ...(typeof projectPath === 'string' ? { projectPath } : {}),
112
+ };
100
113
  });
101
114
  }
102
115
  /**
@@ -116,16 +129,25 @@ export function marketplaceListeLesen(text) {
116
129
  /**
117
130
  * Plugins über `claude plugin …`. Scope ist immer `local` mit dem Arbeitsverzeichnis selbst als
118
131
  * cwd (E27). Nur das Home des Nutzers läuft als `--scope user` ohne cwd.
132
+ *
133
+ * Welche claude-CLI, entscheidet der Ort (E101, derselbe Weg wie MCP seit E100): ein WSL-Heim und
134
+ * ein Arbeitsverzeichnis unter `\\wsl$` gehen an die CLI der Distro, das cwd ist dort der
135
+ * Linux-Pfad. Jede Installation führt ihre eigene Marketplace-Liste, deshalb wirken auch die
136
+ * Marketplace-Operationen in der Welt des genannten Orts. Vor jedem Aufruf prüft die Fähigkeit, ob
137
+ * dort eine CLI antwortet, sonst `ClaudeCliNichtErreichbar`.
119
138
  */
120
139
  class ClaudePlugins {
121
140
  harness;
122
- ausfuehrer;
123
- constructor(harness, ausfuehrer) {
141
+ constructor(harness) {
124
142
  this.harness = harness;
125
- this.ausfuehrer = ausfuehrer;
126
143
  }
127
- async rufen(args, cwd) {
128
- const { stdout, code } = await this.ausfuehrer(args, cwd);
144
+ /** Die claude-CLI des Orts, nachdem feststeht, dass sie antwortet. */
145
+ async cli(ort) {
146
+ await this.harness.cliPruefen(ort);
147
+ return this.harness.ausfuehrerFuer(ort);
148
+ }
149
+ async rufen(ort, args, cwd) {
150
+ const { stdout, code } = await (await this.cli(ort))(args, cwd);
129
151
  if (code !== 0)
130
152
  throw new Error(`claude ${args.join(' ')} scheiterte (Code ${code}): ${stdout.trim()}`);
131
153
  return stdout;
@@ -134,48 +156,80 @@ class ClaudePlugins {
134
156
  * Scope und cwd eines Ortes (E27). Das cwd ist das Arbeitsverzeichnis selbst, nicht sein
135
157
  * Zielverzeichnis: außerhalb eines Git-Repos legt Claude Code die `settings.local.json` im
136
158
  * Startverzeichnis ab, mit dem Zielverzeichnis als cwd landete sie unter `.claude/skills/`.
159
+ * In WSL steht das cwd als Linux-Pfad da (E101).
137
160
  */
138
161
  scope(ziel) {
139
- ohneCliZugriffPruefen(this.harness.name, ziel.ort, 'Plugins');
140
162
  if (ziel.ort.art === 'home')
141
163
  return { scope: 'user' };
142
164
  if (ziel.ort.art === 'arbeitsverzeichnis')
143
- return { scope: 'local', cwd: ziel.ort.pfad };
165
+ return { scope: 'local', cwd: cliPfad(ziel.ort.pfad) };
144
166
  throw new OrtNichtUnterstuetzt(this.harness.name, ziel.ort.art);
145
167
  }
146
- async marketplaceBekannt(name) {
147
- const { stdout, code } = await this.ausfuehrer(['plugin', 'marketplace', 'list']);
168
+ async marketplaceBekannt(name, ort) {
169
+ const { stdout, code } = await (await this.cli(ort))(['plugin', 'marketplace', 'list']);
148
170
  if (code !== 0)
149
171
  return false;
150
172
  return marketplaceListeLesen(stdout).includes(name);
151
173
  }
152
- async marketplaceHinzufuegen(_name, url) {
153
- await this.rufen(['plugin', 'marketplace', 'add', url]);
174
+ /**
175
+ * Eine lokale Quelle steht mit ihrem Windows-Pfad da; die CLI einer Distro braucht ihn in deren
176
+ * Schreibweise (E101): `C:\…` unter der Automount-Wurzel, `\\wsl$\<dieselbe Distro>\…` als
177
+ * Linux-Pfad. URLs und alles andere gehen unverändert durch.
178
+ */
179
+ async marketplaceHinzufuegen(_name, url, ort) {
180
+ await this.rufen(ort, ['plugin', 'marketplace', 'add', await this.urlInWelt(url, ort)]);
181
+ }
182
+ async urlInWelt(url, ort) {
183
+ const distro = wslDistroVonOrt(ort);
184
+ if (distro === undefined)
185
+ return url;
186
+ const unc = wslUncZerlegen(url);
187
+ if (unc !== undefined)
188
+ return unc.distro.toLowerCase() === distro.toLowerCase() ? unc.linuxPfad : url;
189
+ if (/^[a-zA-Z]:[\\/]/.test(url))
190
+ return await this.harness.bruecke(distro).vomHost(url);
191
+ return url;
154
192
  }
155
193
  async installieren(plugin, marketplace, ziel) {
156
194
  const { scope, cwd } = this.scope(ziel);
157
- await this.rufen(['plugin', 'install', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
195
+ await this.rufen(ziel.ort, ['plugin', 'install', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
158
196
  }
159
197
  async entfernen(plugin, marketplace, ziel) {
160
198
  const { scope, cwd } = this.scope(ziel);
161
- await this.rufen(['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
199
+ await this.rufen(ziel.ort, ['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
162
200
  }
163
201
  /** 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']);
202
+ async aktualisieren(ort) {
203
+ await this.rufen(ort, ['plugin', 'marketplace', 'update']);
204
+ }
205
+ async welten() {
206
+ return await this.harness.erreichbareWelten();
166
207
  }
167
208
  /**
168
209
  * Die Liste gilt nur für das cwd des Aufrufs und enthält dort alle Scopes. Zum Ort gehören im
169
210
  * Home die Plugins mit Scope `user`, im Arbeitsverzeichnis die mit `local` und `project`.
211
+ * E102: Die CLI listet die local-Einträge aller Projekte, je mit `projectPath`; am
212
+ * Arbeitsverzeichnis zählen nur die eigenen (ohne `projectPath` wie bisher). Verglichen wird mit
213
+ * dem CLI-Pfad (in WSL der Linux-Pfad) und lokal zusätzlich mit der aufgelösten Form, weil
214
+ * Claude Code den cwd auflöst.
170
215
  */
171
216
  async installierte(ziel) {
172
217
  const { cwd } = this.scope(ziel);
173
- const { stdout, code } = await this.ausfuehrer(['plugin', 'list', '--json'], cwd);
218
+ const { stdout, code } = await (await this.cli(ziel.ort))(['plugin', 'list', '--json'], cwd);
174
219
  if (code !== 0)
175
220
  return [];
176
221
  const erlaubt = ziel.ort.art === 'home' ? ['user'] : ['local', 'project'];
222
+ const eigene = [];
223
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
224
+ eigene.push(cwd ?? ziel.ort.pfad);
225
+ if (wslUncZerlegen(ziel.ort.pfad) === undefined)
226
+ eigene.push(aufgeloest(ziel.ort.pfad));
227
+ }
177
228
  return pluginListeLesen(stdout)
178
229
  .filter((eintrag) => erlaubt.includes(eintrag.scope))
230
+ .filter((eintrag) => ziel.ort.art !== 'arbeitsverzeichnis' ||
231
+ eintrag.projectPath === undefined ||
232
+ eigene.some((pfad) => pfadGleich(pfad, eintrag.projectPath)))
179
233
  .map((eintrag) => pluginSchluessel(eintrag));
180
234
  }
181
235
  }
@@ -222,8 +276,12 @@ function aufgeloest(pfad) {
222
276
  * Windows angelegte Datei leer.
223
277
  */
224
278
  async function projektMcpJsonLesen(verzeichnis) {
279
+ const text = await readFile(path.join(verzeichnis, '.mcp.json'), 'utf8').catch(() => undefined);
280
+ return text === undefined ? {} : mcpServersAusText(text);
281
+ }
282
+ /** `mcpServers` aus dem Text einer `.mcp.json`; kaputtes JSON heißt nichts, eine BOM stört nicht. */
283
+ function mcpServersAusText(text) {
225
284
  try {
226
- const text = await readFile(path.join(verzeichnis, '.mcp.json'), 'utf8');
227
285
  return mcpServersVon(JSON.parse(ohneBom(text)));
228
286
  }
229
287
  catch {
@@ -239,30 +297,60 @@ async function projektMcpJsonLesen(verzeichnis) {
239
297
  * geschrieben wird sie einzig von `geteiltenUebernehmen` auf ausdrücklichen Auftrag (E64). Format
240
298
  * und Verhalten der CLI stehen in HARNESS-FAKTEN.md, geprüft an Claude Code 2.1.261 gegen ein
241
299
  * eigenes CLAUDE_CONFIG_DIR, nie an einer echten Nutzerdatei.
300
+ *
301
+ * Welche CLI und welche Dateien, entscheidet der Ort (E100): ein WSL-Heim und ein Arbeitsverzeichnis
302
+ * unter `\\wsl$` gehen an die claude-CLI der Distro (cwd als Linux-Pfad), und gelesen werden
303
+ * `~/.claude.json` und `.mcp.json` der Distro über die Bridge — die Windows-Dateien wären die
304
+ * falschen. Vor jedem schreibenden Aufruf prüft die Fähigkeit, ob am Ort eine CLI antwortet;
305
+ * sonst hielte das idempotente Aushängen ein „nicht gefunden" der fehlenden CLI für Erfolg.
242
306
  */
243
307
  class ClaudeMcp {
244
308
  harness;
245
- ausfuehrer;
246
309
  /** Das primäre Homedir, jedes Mal frisch: es lässt sich zur Laufzeit umstellen (E78, F89). */
247
310
  home;
248
- constructor(harness, ausfuehrer, home) {
311
+ constructor(harness, home) {
249
312
  this.harness = harness;
250
- this.ausfuehrer = ausfuehrer;
251
313
  this.home = home;
252
314
  }
315
+ /** Die claude-CLI des Orts, nachdem feststeht, dass sie antwortet (E100). */
316
+ async cli(ort) {
317
+ await this.harness.cliPruefen(ort);
318
+ return this.harness.ausfuehrerFuer(ort);
319
+ }
320
+ /** Der Pfad eines Arbeitsverzeichnisses in der Welt seiner CLI: in WSL der Linux-Pfad (E100). */
321
+ cliPfad(pfad) {
322
+ return cliPfad(pfad);
323
+ }
324
+ /** Wo die geteilte `.mcp.json` eines Arbeitsverzeichnisses liegt, in WSL als Linux-Pfad (E100). */
325
+ mcpJsonPfad(pfad) {
326
+ const wsl = wslUncZerlegen(pfad);
327
+ return wsl === undefined ? path.join(pfad, '.mcp.json') : path.posix.join(wsl.linuxPfad, '.mcp.json');
328
+ }
329
+ /** Die geteilte `.mcp.json` eines Arbeitsverzeichnisses, in WSL über die Bridge gelesen (E100). */
330
+ async projektMcpJson(pfad) {
331
+ const wsl = wslUncZerlegen(pfad);
332
+ if (wsl === undefined)
333
+ return await projektMcpJsonLesen(pfad);
334
+ const text = await this.harness.bruecke(wsl.distro).dateiLesen(this.mcpJsonPfad(pfad)).catch(() => undefined);
335
+ return text === undefined ? {} : mcpServersAusText(text);
336
+ }
253
337
  anGitWurzelGebunden = true;
254
338
  ebenen() {
255
339
  return ['home', 'arbeitsverzeichnis'];
256
340
  }
341
+ /** Ob am Ort eine claude-CLI antwortet (E103): ohne sie bringt Einhängen oder Aushängen dort nichts. */
342
+ async ortBedienbar(ort) {
343
+ return await this.harness.cliErreichbar(ort);
344
+ }
257
345
  hinweisNachAenderung() {
258
346
  return 'Claude Code übernimmt die Änderung mit der nächsten Sitzung.';
259
347
  }
348
+ /** Scope und cwd eines Ortes; das cwd steht in der Welt der zuständigen CLI (E100). */
260
349
  scope(ort) {
261
- ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
262
350
  if (ort.art === 'home')
263
351
  return { scope: 'user' };
264
352
  if (ort.art === 'arbeitsverzeichnis')
265
- return { scope: 'local', cwd: ort.pfad };
353
+ return { scope: 'local', cwd: this.cliPfad(ort.pfad) };
266
354
  throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
267
355
  }
268
356
  /**
@@ -273,8 +361,9 @@ class ClaudeMcp {
273
361
  */
274
362
  async einhaengen(name, konfig, ort) {
275
363
  const { scope, cwd } = this.scope(ort);
364
+ const cli = await this.cli(ort);
276
365
  const json = claudeMcpJson(konfig);
277
- const { stdout, code } = await this.ausfuehrer(['mcp', 'add-json', name, json, '--scope', scope], cwd);
366
+ const { stdout, code } = await cli(['mcp', 'add-json', name, json, '--scope', scope], cwd);
278
367
  if (code === 0)
279
368
  return;
280
369
  const bestehend = await this.eintrag(name, ort);
@@ -294,15 +383,16 @@ class ClaudeMcp {
294
383
  */
295
384
  async aushaengen(name, ort) {
296
385
  const { scope, cwd } = this.scope(ort);
297
- const { stdout, code } = await this.ausfuehrer(['mcp', 'remove', name, '--scope', scope], cwd);
386
+ const cli = await this.cli(ort);
387
+ const { stdout, code } = await cli(['mcp', 'remove', name, '--scope', scope], cwd);
298
388
  if (code === 0)
299
389
  return;
300
390
  if ((await this.eintrag(name, ort)) !== undefined)
301
391
  throw new Error(`claude mcp remove ${name} scheiterte (Code ${code}): ${stdout.trim()}`);
302
392
  if (ort.art === 'arbeitsverzeichnis') {
303
- const geteilt = await projektMcpJsonLesen(ort.pfad);
393
+ const geteilt = await this.projektMcpJson(ort.pfad);
304
394
  if (name in geteilt)
305
- throw new McpNurGeteilterFundort({ harness: this.harness.name, ort }, name, path.join(ort.pfad, '.mcp.json'));
395
+ throw new McpNurGeteilterFundort({ harness: this.harness.name, ort }, name, this.mcpJsonPfad(ort.pfad));
306
396
  }
307
397
  }
308
398
  async eingehaengte(ort) {
@@ -310,16 +400,14 @@ class ClaudeMcp {
310
400
  }
311
401
  /**
312
402
  * 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.
403
+ * weder beim Ein- noch beim Aushängen angefasst. Home kennt keine solche Datei. In WSL liegt sie
404
+ * im Linux-Dateisystem und wird über die Bridge gelesen, der Pfad steht als Linux-Pfad da (E100).
314
405
  */
315
406
  async geteilteFundorte(ort) {
316
407
  if (ort.art !== 'arbeitsverzeichnis')
317
408
  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);
409
+ const datei = this.mcpJsonPfad(ort.pfad);
410
+ const server = await this.projektMcpJson(ort.pfad);
323
411
  return Object.keys(server).map((name) => ({ name, datei, scope: 'project' }));
324
412
  }
325
413
  /**
@@ -341,12 +429,13 @@ class ClaudeMcp {
341
429
  const ziel = { harness: this.harness.name, ort };
342
430
  if (ort.art !== 'arbeitsverzeichnis')
343
431
  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];
432
+ const datei = this.mcpJsonPfad(ort.pfad);
433
+ const block = (await this.projektMcpJson(ort.pfad))[name];
347
434
  if (block === undefined)
348
435
  throw new McpNichtInGeteilterDatei(ziel, name, datei);
349
- const hin = await this.ausfuehrer(['mcp', 'add-json', name, JSON.stringify(block), '--scope', 'local'], ort.pfad);
436
+ const cli = await this.cli(ort);
437
+ const cwd = this.cliPfad(ort.pfad);
438
+ const hin = await cli(['mcp', 'add-json', name, JSON.stringify(block), '--scope', 'local'], cwd);
350
439
  if (hin.code !== 0) {
351
440
  // Der einzige harmlose Fehlschlag: derselbe Block liegt schon im privaten Scope, etwa weil ein
352
441
  // früherer Lauf zwischen den beiden Schritten abgebrochen ist. Ein abweichender Block wird
@@ -357,10 +446,10 @@ class ClaudeMcp {
357
446
  if (!tiefGleich(bestehend, block))
358
447
  throw new McpAbweichenderEintrag(ziel, name);
359
448
  }
360
- const weg = await this.ausfuehrer(['mcp', 'remove', name, '--scope', 'project'], ort.pfad);
449
+ const weg = await cli(['mcp', 'remove', name, '--scope', 'project'], cwd);
361
450
  // Nicht der Code entscheidet, sondern die Datei danach: steht der Name nicht mehr drin, ist die
362
451
  // Übernahme fertig, egal wie die CLI den Fall genannt hat.
363
- if (weg.code !== 0 && name in (await projektMcpJsonLesen(ort.pfad)))
452
+ if (weg.code !== 0 && name in (await this.projektMcpJson(ort.pfad)))
364
453
  throw new McpUebernahmeUnvollstaendig(ziel, name, datei, weg.stdout.trim());
365
454
  return { datei };
366
455
  }
@@ -375,23 +464,35 @@ class ClaudeMcp {
375
464
  * Ordner in einem Git-Repo, ist der Schlüssel die Git-Wurzel, nicht der Ordner (E91, belegt an
376
465
  * 2.1.285): für einen Unterordner liest diese Methode deshalb nichts, und der Kern sperrt dort
377
466
  * die MCP-Operationen, bevor sie den Adapter erreichen.
467
+ *
468
+ * In WSL (E100) ist es die `~/.claude.json` der Distro, über die Bridge gelesen; der Schlüssel ist
469
+ * der Linux-Pfad, zusätzlich in der Distro aufgelöst (`realpath`).
378
470
  */
379
471
  async mcpServersAm(ort) {
380
- ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
381
- const daten = await this.claudeJsonLesen();
472
+ const distro = wslDistroVonOrt(ort);
473
+ const daten = await this.claudeJsonLesen(distro);
382
474
  if (ort.art === 'home')
383
475
  return mcpServersVon(daten);
384
476
  if (ort.art === 'arbeitsverzeichnis') {
385
477
  const projects = objektFeld(daten, 'projects');
386
- const projekt = objektFeld(projects, ort.pfad) ?? objektFeld(projects, aufgeloest(ort.pfad));
387
- return mcpServersVon(projekt);
478
+ const pfad = this.cliPfad(ort.pfad);
479
+ const direkt = objektFeld(projects, pfad);
480
+ if (direkt !== undefined)
481
+ return mcpServersVon(direkt);
482
+ const echt = distro === undefined
483
+ ? aufgeloest(pfad)
484
+ : await this.harness.bruecke(distro).aufgeloest(pfad).catch(() => undefined);
485
+ return mcpServersVon(echt === undefined ? undefined : objektFeld(projects, echt));
388
486
  }
389
487
  throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
390
488
  }
391
- async claudeJsonLesen() {
489
+ /** `~/.claude.json`: unter Windows die im primären Homedir, in WSL die der Distro (E100). */
490
+ async claudeJsonLesen(distro) {
392
491
  try {
393
- const text = await readFile(path.join(this.home(), '.claude.json'), 'utf8');
394
- return JSON.parse(ohneBom(text));
492
+ const text = distro === undefined
493
+ ? await readFile(path.join(this.home(), '.claude.json'), 'utf8')
494
+ : await this.harness.bruecke(distro).claudeJsonLesen();
495
+ return text === undefined ? undefined : JSON.parse(ohneBom(text));
395
496
  }
396
497
  catch {
397
498
  return undefined;
@@ -404,42 +505,145 @@ export class ClaudeCode {
404
505
  ausfuehrer;
405
506
  /** Zugriff je Ort (E78): lokal wie bisher, für ein WSL-Heim die Bridge. */
406
507
  zugriffFuer;
407
- /** Wird von `vorhanden()` gesetzt, sobald die claude-CLI auf dem PATH liegt. */
508
+ /** Die WSL-Anbindung (E100): claude-CLI je Ort, Bridge je Distro, Distros der eingetragenen Orte. */
509
+ wsl;
510
+ /**
511
+ * Plugins (E16) und MCP-Server (E61) setzt `vorhanden()`, sobald irgendwo eine claude-CLI
512
+ * antwortet (E100, E101): unter Windows oder in der Distro eines eingetragenen WSL-Orts. Ob es an
513
+ * einem bestimmten Ort geht, entscheidet der Ort (`cliPruefen`).
514
+ */
408
515
  plugins;
409
- /** Wird von `vorhanden()` gesetzt, sobald die claude-CLI auf dem PATH liegt (E61). */
410
516
  mcp;
411
- geprueft = false;
517
+ /**
518
+ * Ob die claude-CLI antwortet, je Welt: Schlüssel '' für Windows, sonst die Distro. Ein positives
519
+ * Ergebnis gilt für die Lebenszeit des Adapters, ein negatives verfällt nach 60 Sekunden (E103), damit
520
+ * eine laufende GUI eine inzwischen installierte claude findet. Solange `bis` fehlt (die Antwort steht
521
+ * aus) oder noch nicht erreicht ist, teilen sich parallele Anfragen das Versprechen.
522
+ */
523
+ erreichbarkeit = new Map();
412
524
  /**
413
525
  * `heime` sagt, wo die Homedirs liegen (E78): das primäre und, seit F87, zusätzliche in einer
414
526
  * WSL-Distro. Ein blanker Pfad steht weiterhin für „nur dieses eine Home", damit Tests und
415
- * Aufrufer ohne Zustand unverändert bleiben.
527
+ * Aufrufer ohne Zustand unverändert bleiben. `wsl` verdrahtet `bauen` (E100); ohne Angabe gehen
528
+ * die Aufrufe an das echte wsl.exe, und als WSL-Orte zählen nur die zusätzlichen Heime.
416
529
  */
417
- constructor(heime, ausfuehrer = claudeAusfuehrer, zugriffFuer) {
530
+ constructor(heime, ausfuehrer = claudeAusfuehrer, zugriffFuer, wsl) {
418
531
  this.heime = typeof heime === 'string' ? nurHome(heime) : heime;
419
532
  this.ausfuehrer = ausfuehrer;
420
533
  const lokal = new LokalerZugriff();
421
534
  this.zugriffFuer = zugriffFuer ?? (() => lokal);
535
+ const bruecken = new Map();
536
+ const langeBridge = wslAusfuehrerMit(120_000);
537
+ this.wsl = {
538
+ ausfuehrerFuer: wsl?.ausfuehrerFuer ??
539
+ ((ort) => {
540
+ const distro = wslDistroVonOrt(ort);
541
+ return distro === undefined ? this.ausfuehrer : wslClaudeAusfuehrer(distro, langeBridge);
542
+ }),
543
+ bruecke: wsl?.bruecke ??
544
+ ((distro) => {
545
+ let b = bruecken.get(distro);
546
+ if (b === undefined)
547
+ bruecken.set(distro, (b = new WslBruecke(distro)));
548
+ return b;
549
+ }),
550
+ jetzt: wsl?.jetzt ?? (() => Date.now()),
551
+ distros: wsl?.distros ??
552
+ (async () => this.heime
553
+ .zusaetzliche(this.name)
554
+ .map((heim) => wslDistroVonOrt({ art: 'home', heim: heim.adresse }))
555
+ .filter((distro) => distro !== undefined)),
556
+ };
557
+ }
558
+ /** Die claude-CLI, die für diesen Ort zuständig ist (E100): die des Rechners oder die der Distro. */
559
+ ausfuehrerFuer(ort) {
560
+ return this.wsl.ausfuehrerFuer(ort);
561
+ }
562
+ /** Die Bridge in eine Distro, für das Lesen ihrer Dateien (E100). */
563
+ bruecke(distro) {
564
+ return this.wsl.bruecke(distro);
565
+ }
566
+ /**
567
+ * Ob an diesem Ort eine claude-CLI antwortet (`claude --version`), je Welt: ein „ja" bleibt, ein
568
+ * „nein" gilt 60 Sekunden (E103).
569
+ */
570
+ async erreichbar(ort) {
571
+ const schluessel = wslDistroVonOrt(ort) ?? '';
572
+ const bekannt = this.erreichbarkeit.get(schluessel);
573
+ if (bekannt !== undefined && (bekannt.bis === undefined || this.wsl.jetzt() < bekannt.bis))
574
+ return await bekannt.versprechen;
575
+ const cli = schluessel === '' ? this.ausfuehrer : this.ausfuehrerFuer(ort);
576
+ const eintrag = {
577
+ versprechen: cli(['--version'])
578
+ .then(({ code }) => code === 0)
579
+ .catch(() => false)
580
+ .then((ok) => {
581
+ if (!ok)
582
+ eintrag.bis = this.wsl.jetzt() + ERREICHBARKEIT_NEGATIV_MS;
583
+ return ok;
584
+ }),
585
+ };
586
+ this.erreichbarkeit.set(schluessel, eintrag);
587
+ return await eintrag.versprechen;
588
+ }
589
+ /** Ob an diesem Ort eine claude-CLI antwortet, ohne zu werfen (E103): für Plätze, die ohne CLI nichts bringen. */
590
+ async cliErreichbar(ort) {
591
+ return await this.erreichbar(ort);
592
+ }
593
+ /** Wirft `ClaudeCliNichtErreichbar`, wenn an diesem Ort keine claude-CLI antwortet (E100). */
594
+ async cliPruefen(ort) {
595
+ if (!(await this.erreichbar(ort)))
596
+ throw new ClaudeCliNichtErreichbar({ harness: this.name, ort }, wslDistroVonOrt(ort));
422
597
  }
423
598
  /**
424
599
  * Claude Code liegt auf dem Rechner, wenn es sein Home angelegt hat — oder wenn ein zusätzliches
425
600
  * 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.
601
+ * Windows-Home ohne `.claude` soll die WSL-Ziele nicht mit verschwinden lassen.
602
+ *
603
+ * Die Plugin- und die MCP-Fähigkeit (E100, E101) kommen dazu, sobald `claude --version` unter
604
+ * Windows durchläuft — oder, wenn dort keine CLI liegt, sobald sie in der Distro eines
605
+ * eingetragenen WSL-Orts antwortet. Die Distros werden erst dann befragt, nacheinander bis zur
606
+ * ersten Antwort, damit ein Rechner mit Windows-claude keinen wsl.exe-Start zahlt. Jede Welt wird
607
+ * einmal geprüft (ein „ja" bleibt); in der GUI kommt ein später eingetragener WSL-Ort beim nächsten Aufruf
608
+ * dazu, ohne die schon geprüften erneut zu fragen. Ein negatives Ergebnis verfällt nach 60 Sekunden
609
+ * (E103): ein erneuter Aufruf danach fragt noch einmal und setzt die Fähigkeiten, sobald claude
610
+ * inzwischen da ist.
428
611
  */
429
612
  async vorhanden() {
430
613
  const primaer = this.heime.primaer(this.name);
431
614
  const da = (await stat(path.join(primaer, CLAUDE_VERZEICHNIS)).catch(() => undefined)) !== undefined ||
432
615
  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
- }
616
+ if (this.mcp === undefined && (await this.irgendwoErreichbar())) {
617
+ this.plugins = new ClaudePlugins(this);
618
+ this.mcp = new ClaudeMcp(this, () => this.heime.primaer(this.name));
440
619
  }
441
620
  return da;
442
621
  }
622
+ /** Antwortet irgendwo eine claude-CLI: unter Windows oder in einer Distro der WSL-Orte (E100). */
623
+ async irgendwoErreichbar() {
624
+ if (await this.erreichbar({ art: 'home' }))
625
+ return true;
626
+ const distros = await this.wsl.distros().catch(() => []);
627
+ for (const distro of new Set(distros))
628
+ if (await this.erreichbar(distroOrt(distro)))
629
+ return true;
630
+ return false;
631
+ }
632
+ /**
633
+ * Je Welt mit antwortender claude-CLI ein Ort, der für sie steht (E101): das Windows-Home und je
634
+ * Distro der eingetragenen WSL-Orte ihr Heim. Anders als `irgendwoErreichbar` fragt es alle
635
+ * Distros, auch wenn Windows schon antwortet — jede Welt hat ihre eigenen Marketplaces.
636
+ */
637
+ async erreichbareWelten() {
638
+ const welten = [];
639
+ if (await this.erreichbar({ art: 'home' }))
640
+ welten.push({ art: 'home' });
641
+ const distros = await this.wsl.distros().catch(() => []);
642
+ for (const distro of new Set(distros))
643
+ if (await this.erreichbar(distroOrt(distro)))
644
+ welten.push(distroOrt(distro));
645
+ return welten;
646
+ }
443
647
  orte() {
444
648
  return ['home', 'arbeitsverzeichnis'];
445
649
  }
@@ -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>
@@ -1202,6 +1202,23 @@
1202
1202
  var verzMcpZiele = MCP_PLAETZE.filter(function (p) {
1203
1203
  return p.ort.art === 'arbeitsverzeichnis' && p.ort.pfad === pfad;
1204
1204
  });
1205
+ // E103: mcpPlaetze() lässt Orte weg, an denen der Harness nicht bedienbar ist (keine CLI). Hängt
1206
+ // dort schon etwas, bleibt der Abschnitt samt Entfernen-Knopf trotzdem stehen; nur das
1207
+ // „hinzufügen" entfällt (mcpKannAn).
1208
+ var gesehen = {};
1209
+ verzMcpZiele.forEach(function (p) { gesehen[zielKey(p)] = true; });
1210
+ var vorhandene = [];
1211
+ MCP_KATALOG.forEach(function (k) {
1212
+ k.eingehaengt.forEach(function (z) { vorhandene.push(z); });
1213
+ k.geteilteFundorte.forEach(function (g) { vorhandene.push(g.ziel); });
1214
+ });
1215
+ MCP_FREMD.forEach(function (x) { vorhandene.push(x.ziel); });
1216
+ MCP_FREMD_GETEILT.forEach(function (x) { vorhandene.push(x.ziel); });
1217
+ vorhandene.forEach(function (z) {
1218
+ if (z.ort.art !== 'arbeitsverzeichnis' || z.ort.pfad !== pfad || gesehen[zielKey(z)]) return;
1219
+ gesehen[zielKey(z)] = true;
1220
+ verzMcpZiele.push(z);
1221
+ });
1205
1222
 
1206
1223
  if (reihenfolge.length === 0) {
1207
1224
  var leer = e('div', 'leerzeile');
@@ -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
  }
@@ -483,10 +499,15 @@ export class Anwendungsfaelle {
483
499
  * Die Plätze für den Einhängen-Dialog der GUI (E61-Folgerunde, F42): dieselbe Liste wie in
484
500
  * `mcpUebersicht`, aber roh, ohne den Abgleich mit Katalog-Einträgen. Die Fähigkeit hängt am
485
501
  * Adapter, nicht am Harness-Namen (E16): agents kommt nie vor, weil es `mcp` nicht implementiert.
502
+ * Orte, an denen der Harness nicht bedienbar ist (E103, etwa keine claude-CLI), fehlen; die
503
+ * Übersicht lässt sie stehen, damit vorhandene Einträge sichtbar bleiben.
486
504
  */
487
505
  async mcpPlaetze() {
488
506
  const plaetze = [];
489
507
  for (const ziel of await this.mcpZiele()) {
508
+ const mcp = this.harnessMitMcp(ziel.harness).mcp;
509
+ if (mcp.ortBedienbar !== undefined && !(await mcp.ortBedienbar(ziel.ort)))
510
+ continue;
490
511
  const wurzel = await this.mcpGitWurzelFuer(this.harnessMitMcp(ziel.harness).mcp, ziel);
491
512
  plaetze.push(wurzel === null ? ziel : { ...ziel, gitWurzel: wurzel });
492
513
  }
@@ -1526,8 +1547,9 @@ export class Anwendungsfaelle {
1526
1547
  if (!harness.orte().includes(ziel.ort.art))
1527
1548
  throw new OrtNichtUnterstuetzt(harness.name, ziel.ort.art);
1528
1549
  if (kannPlugins(harness)) {
1529
- if (!(await harness.plugins.marketplaceBekannt(fund.plugin.marketplace)))
1530
- await harness.plugins.marketplaceHinzufuegen(fund.plugin.marketplace, marketplaceUrl(fund.quelle));
1550
+ // Die Marketplace-Liste gehört zur Welt des Ziels (E101): ein WSL-Ziel trägt ihn in der Distro ein.
1551
+ if (!(await harness.plugins.marketplaceBekannt(fund.plugin.marketplace, ziel.ort)))
1552
+ await harness.plugins.marketplaceHinzufuegen(fund.plugin.marketplace, marketplaceUrl(fund.quelle), ziel.ort);
1531
1553
  await harness.vorbereiten(ziel.ort);
1532
1554
  await harness.plugins.installieren(fund.plugin.name, fund.plugin.marketplace, ziel);
1533
1555
  return { plugin: fund.plugin, art: 'installiert', skills: [], imZeiger: false };
@@ -1860,13 +1882,26 @@ export class Anwendungsfaelle {
1860
1882
  }
1861
1883
  this.nachgezogen.add(quelleSchluessel(quelle));
1862
1884
  }
1863
- // Nach den Checkouts die Plugins nachziehen (E14).
1885
+ // Nach den Checkouts die Plugins nachziehen (E14), je Welt mit erreichbarer Installation
1886
+ // (E101): Windows und jede Distro der eingetragenen WSL-Orte haben ihre eigenen Marketplaces.
1887
+ // Ein Fehlschlag in einer Welt steht im Ergebnis und bricht den Lauf nicht ab.
1864
1888
  const pluginHarnesses = [];
1889
+ const pluginFehler = [];
1865
1890
  for (const harness of nurSchluessel !== undefined ? [] : await this.vorhandeneHarnesses()) {
1866
1891
  if (!kannPlugins(harness))
1867
1892
  continue;
1868
- await harness.plugins.aktualisieren();
1869
- pluginHarnesses.push(harness.name);
1893
+ let erfolgreich = false;
1894
+ for (const ort of await harness.plugins.welten()) {
1895
+ try {
1896
+ await harness.plugins.aktualisieren(ort);
1897
+ erfolgreich = true;
1898
+ }
1899
+ catch (fehler) {
1900
+ pluginFehler.push({ ziel: { harness: harness.name, ort }, fehler: fehlertext(fehler) });
1901
+ }
1902
+ }
1903
+ if (erfolgreich)
1904
+ pluginHarnesses.push(harness.name);
1870
1905
  }
1871
1906
  // Bundles vor dem Aufräumen (E69, F64=O1): der Abgleich legt und entfernt Links, und erst
1872
1907
  // danach steht fest, in welchen Checkout noch ein Link zeigt. Ein Fehlschlag je Bundle steckt
@@ -1878,15 +1913,16 @@ export class Anwendungsfaelle {
1878
1913
  const doctorErgebnis = nurSchluessel !== undefined
1879
1914
  ? { fehlendeArbeitsverzeichnisse: [], vergessen: [] }
1880
1915
  : await this.doctor({ rueckfragen });
1881
- await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis));
1882
- return { checkouts, pluginHarnesses, bundles, aufraeumen, doctor: doctorErgebnis };
1916
+ await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis));
1917
+ return { checkouts, pluginHarnesses, pluginFehler, bundles, aufraeumen, doctor: doctorErgebnis };
1883
1918
  }
1884
1919
  /**
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.
1920
+ * Ereignisse eines Aktualisieren-Laufs für den Verlauf (E80): eine Plugin-Änderung bleibt
1921
+ * draußen, weil sie sich nicht messen lässt (`claude plugin marketplace update` liefert keine
1922
+ * Auskunft) — die bisherige Zeile „Plugins aktualisiert" war deshalb geschönt. Ein Fehlschlag
1923
+ * je Welt (E101) steht dagegen drin.
1888
1924
  */
1889
- aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis) {
1925
+ aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis) {
1890
1926
  const ereignisse = [];
1891
1927
  for (const { checkout, aktualisierung, geaenderteSkills } of checkouts) {
1892
1928
  if (aktualisierung.ergebnis === 'aktualisiert')
@@ -1905,6 +1941,8 @@ export class Anwendungsfaelle {
1905
1941
  fehler: aktualisierung.fehler,
1906
1942
  });
1907
1943
  }
1944
+ for (const { ziel, fehler } of pluginFehler)
1945
+ ereignisse.push({ art: 'fehler', betrifft: `Plugins ${ziel.harness} ${pluginWeltText(ziel.ort)}`, fehler });
1908
1946
  for (const eintrag of bundles) {
1909
1947
  if (eintrag.ergebnis === 'fehlgeschlagen')
1910
1948
  ereignisse.push({ art: 'fehler', betrifft: eintrag.bundle, fehler: eintrag.fehler ?? '' });
@@ -2365,20 +2403,18 @@ export class Anwendungsfaelle {
2365
2403
  }
2366
2404
  // Installierte Plugins je Ziel vorab, mit begrenzter Parallelität (E93): `claude plugin list`
2367
2405
  // 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.
2406
+ // Arbeitsverzeichnissen Sekunden. WSL-Ziele fragen die claude-CLI ihrer Distro (E101); ein Ort
2407
+ // ohne erreichbare CLI ergibt eine leere Liste.
2371
2408
  const pluginListen = new Map();
2372
2409
  if (mitPlugins) {
2373
2410
  const abfragen = [];
2374
2411
  for (const { harness, ziele } of plan)
2375
2412
  if (kannPlugins(harness))
2376
2413
  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
- });
2414
+ abfragen.push({
2415
+ schluessel: zielSchluessel(ziel),
2416
+ abruf: () => harness.plugins.installierte(ziel).catch(() => []),
2417
+ });
2382
2418
  const listen = await mitBegrenzung(PLUGIN_ABFRAGEN_GLEICHZEITIG, abfragen.map((a) => a.abruf));
2383
2419
  abfragen.forEach((a, i) => pluginListen.set(a.schluessel, listen[i]));
2384
2420
  }
@@ -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.30",
3
+ "version": "0.0.35",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",