hmgr 0.0.26 → 0.0.28

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.
@@ -1,22 +1,27 @@
1
1
  // Das universelle Skill-Verzeichnis `.agents/skills`: ein Harness-Typ ohne Programm dahinter (E35).
2
2
  // Home ist `~/.agents/skills`, das Arbeitsverzeichnis `<pfad>/.agents/skills`.
3
- import { mkdir, stat } from 'node:fs/promises';
3
+ import { stat } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { OrtNichtUnterstuetzt } from "../kern/modell.js";
6
6
  import { gitignoreEintragen, istGitRepo } from "./gitignore.js";
7
- import { nurHome } from "./heime.js";
7
+ import { LokalerZugriff } from "./dateisystem.js";
8
+ import { arbeitsverzeichnisVerbinden, nurHome } from "./heime.js";
8
9
  const AGENTS_VERZEICHNIS = '.agents';
9
10
  const SKILLS_ORDNER = 'skills';
10
11
  const GITIGNORE_ZEILE = '.agents/';
11
12
  export class Agents {
12
13
  name = 'agents';
13
14
  heime;
15
+ /** Zugriff je Ort (E96): lokal, für ein Arbeitsverzeichnis in WSL die Bridge der Distro. */
16
+ zugriffFuer;
14
17
  /**
15
18
  * `heime` sagt, wo das Homedir liegt (E78, F89): umstellbar, aber ohne zusätzliche Homedirs —
16
19
  * die gibt es bisher nur bei claude-code. Ein blanker Pfad steht für „genau dieses Home".
17
20
  */
18
- constructor(heime) {
21
+ constructor(heime, zugriffFuer) {
19
22
  this.heime = typeof heime === 'string' ? nurHome(heime) : heime;
23
+ const lokal = new LokalerZugriff();
24
+ this.zugriffFuer = zugriffFuer ?? (() => lokal);
20
25
  }
21
26
  get home() {
22
27
  return this.heime.primaer(this.name);
@@ -37,11 +42,11 @@ export class Agents {
37
42
  if (ort.art === 'home')
38
43
  return path.join(this.heime.pfad(this.name, ort), AGENTS_VERZEICHNIS, SKILLS_ORDNER);
39
44
  if (ort.art === 'arbeitsverzeichnis')
40
- return path.join(ort.pfad, AGENTS_VERZEICHNIS, SKILLS_ORDNER);
45
+ return arbeitsverzeichnisVerbinden(ort.pfad, AGENTS_VERZEICHNIS, SKILLS_ORDNER);
41
46
  throw new OrtNichtUnterstuetzt(this.name, ort.art);
42
47
  }
43
48
  async vorbereiten(ort) {
44
- await mkdir(this.zielVerzeichnis(ort), { recursive: true });
49
+ await this.zugriffFuer(ort).verzeichnisAnlegen(this.zielVerzeichnis(ort));
45
50
  // .agents/ darf nicht ins Repo (B7).
46
51
  if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
47
52
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
@@ -3,11 +3,11 @@ 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 { HeimOhneCliZugriff, McpAbweichenderEintrag, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, OrtNichtUnterstuetzt, pluginSchluessel, } from "../kern/modell.js";
6
+ import { ArbeitsverzeichnisOhneCliZugriff, HeimOhneCliZugriff, McpAbweichenderEintrag, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, OrtNichtUnterstuetzt, pluginSchluessel, 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
- import { heimVerbinden, nurHome } from "./heime.js";
10
+ import { arbeitsverzeichnisVerbinden, heimVerbinden, nurHome } from "./heime.js";
11
11
  import { tiefGleich } from "./vergleich.js";
12
12
  const CLAUDE_VERZEICHNIS = '.claude';
13
13
  const SKILLS_ORDNER = 'skills';
@@ -19,6 +19,17 @@ const SKILLS_ORDNER = 'skills';
19
19
  function zusaetzlichesHeim(ort) {
20
20
  return ort.art === 'home' ? ort.heim : undefined;
21
21
  }
22
+ /**
23
+ * MCP und Plugins gehen nur über die claude-CLI dieses Rechners (E61, E78, E96): ein zusätzliches
24
+ * Homedir und ein Arbeitsverzeichnis in WSL liegen außerhalb ihrer Konfiguration.
25
+ */
26
+ function ohneCliZugriffPruefen(harness, ort, was) {
27
+ const heim = zusaetzlichesHeim(ort);
28
+ if (heim !== undefined)
29
+ throw new HeimOhneCliZugriff(harness, heim, was);
30
+ if (ort.art === 'arbeitsverzeichnis' && wslUncZerlegen(ort.pfad) !== undefined)
31
+ throw new ArbeitsverzeichnisOhneCliZugriff(harness, ort.pfad, was);
32
+ }
22
33
  const GITIGNORE_ZEILE = '.claude/';
23
34
  /**
24
35
  * Standard-Ausführer: `claude` vom PATH. Ein Fehlschlag wird zum Code, nicht zur Ausnahme.
@@ -125,9 +136,7 @@ class ClaudePlugins {
125
136
  * Startverzeichnis ab, mit dem Zielverzeichnis als cwd landete sie unter `.claude/skills/`.
126
137
  */
127
138
  scope(ziel) {
128
- const heim = zusaetzlichesHeim(ziel.ort);
129
- if (heim !== undefined)
130
- throw new HeimOhneCliZugriff(this.harness.name, heim, 'Plugins');
139
+ ohneCliZugriffPruefen(this.harness.name, ziel.ort, 'Plugins');
131
140
  if (ziel.ort.art === 'home')
132
141
  return { scope: 'user' };
133
142
  if (ziel.ort.art === 'arbeitsverzeichnis')
@@ -249,9 +258,7 @@ class ClaudeMcp {
249
258
  return 'Claude Code übernimmt die Änderung mit der nächsten Sitzung.';
250
259
  }
251
260
  scope(ort) {
252
- const heim = zusaetzlichesHeim(ort);
253
- if (heim !== undefined)
254
- throw new HeimOhneCliZugriff(this.harness.name, heim, 'MCP-Server');
261
+ ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
255
262
  if (ort.art === 'home')
256
263
  return { scope: 'user' };
257
264
  if (ort.art === 'arbeitsverzeichnis')
@@ -308,6 +315,9 @@ class ClaudeMcp {
308
315
  async geteilteFundorte(ort) {
309
316
  if (ort.art !== 'arbeitsverzeichnis')
310
317
  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 [];
311
321
  const datei = path.join(ort.pfad, '.mcp.json');
312
322
  const server = await projektMcpJsonLesen(ort.pfad);
313
323
  return Object.keys(server).map((name) => ({ name, datei, scope: 'project' }));
@@ -331,6 +341,7 @@ class ClaudeMcp {
331
341
  const ziel = { harness: this.harness.name, ort };
332
342
  if (ort.art !== 'arbeitsverzeichnis')
333
343
  throw new McpUebernahmeNichtMoeglich(ziel);
344
+ ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
334
345
  const datei = path.join(ort.pfad, '.mcp.json');
335
346
  const block = (await projektMcpJsonLesen(ort.pfad))[name];
336
347
  if (block === undefined)
@@ -366,9 +377,7 @@ class ClaudeMcp {
366
377
  * die MCP-Operationen, bevor sie den Adapter erreichen.
367
378
  */
368
379
  async mcpServersAm(ort) {
369
- const heim = zusaetzlichesHeim(ort);
370
- if (heim !== undefined)
371
- throw new HeimOhneCliZugriff(this.harness.name, heim, 'MCP-Server');
380
+ ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
372
381
  const daten = await this.claudeJsonLesen();
373
382
  if (ort.art === 'home')
374
383
  return mcpServersVon(daten);
@@ -442,13 +451,15 @@ export class ClaudeCode {
442
451
  if (ort.art === 'home')
443
452
  return heimVerbinden(ort, this.heime.pfad(this.name, ort), CLAUDE_VERZEICHNIS, SKILLS_ORDNER);
444
453
  if (ort.art === 'arbeitsverzeichnis')
445
- return path.join(ort.pfad, CLAUDE_VERZEICHNIS, SKILLS_ORDNER);
454
+ return arbeitsverzeichnisVerbinden(ort.pfad, CLAUDE_VERZEICHNIS, SKILLS_ORDNER);
446
455
  throw new OrtNichtUnterstuetzt(this.name, ort.art);
447
456
  }
448
457
  async vorbereiten(ort) {
449
458
  // Über den Zugriff, nicht über `mkdir`: in einem WSL-Heim liegt das Verzeichnis im
450
459
  // Linux-Dateisystem, wo Windows-Node nicht anlegen kann (E78).
451
460
  await this.zugriffFuer(ort).verzeichnisAnlegen(this.zielVerzeichnis(ort));
461
+ // `.git` und `.gitignore` sind gewöhnliche Dateien im Arbeitsverzeichnis und bleiben auch bei
462
+ // einem WSL-Pfad über den Share unter `ort.pfad` erreichbar, nicht über den Linux-Pfad (E96).
452
463
  if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
453
464
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
454
465
  }
@@ -51,6 +51,9 @@ export function linkFehlerHinweis(plattform = process.platform) {
51
51
  */
52
52
  export class LokalerZugriff {
53
53
  trenner = path.sep;
54
+ linkFehlerHinweis() {
55
+ return linkFehlerHinweis();
56
+ }
54
57
  verbinden(verzeichnis, name) {
55
58
  return path.join(verzeichnis, name);
56
59
  }
@@ -213,7 +216,7 @@ export class DateisystemZiele {
213
216
  await zugriff.linkAnlegen(pfad, checkoutZiel);
214
217
  }
215
218
  catch (ursache) {
216
- throw new LinkAnlegenFehlgeschlagen(pfad, checkoutZiel, ursache instanceof Error ? ursache.message : String(ursache), linkFehlerHinweis());
219
+ throw new LinkAnlegenFehlgeschlagen(pfad, checkoutZiel, ursache instanceof Error ? ursache.message : String(ursache), zugriff.linkFehlerHinweis());
217
220
  }
218
221
  }
219
222
  async linkEntfernen(ziel, verzeichnis, name) {
@@ -1,6 +1,6 @@
1
1
  // Homedirs eines Harness aus dem Zustand (E78). Synchron, siehe HeimePort.
2
2
  import path from 'node:path';
3
- import { HeimUnbekannt, OrtNichtUnterstuetzt, heimAdresseZerlegen } from "../kern/modell.js";
3
+ import { HeimUnbekannt, OrtNichtUnterstuetzt, heimAdresseZerlegen, wslUncZerlegen } from "../kern/modell.js";
4
4
  /**
5
5
  * Pfade in der Welt eines Homedirs zusammensetzen (E78): POSIX in einer WSL-Distro, sonst die
6
6
  * Schreibweise des Betriebssystems. Ohne das ergäbe `path.join` unter Windows `\`-Pfade für ein
@@ -12,6 +12,14 @@ export function heimVerbinden(ort, ...teile) {
12
12
  ? path.posix.join(...teile)
13
13
  : path.join(...teile);
14
14
  }
15
+ /**
16
+ * Pfad unterhalb eines Arbeitsverzeichnisses (E96). Liegt es in WSL (`\\wsl$\<distro>\…`), kommt der
17
+ * POSIX-Pfad in der Distro heraus, den die Bridge bedient; sonst `path.join` am gespeicherten Pfad.
18
+ */
19
+ export function arbeitsverzeichnisVerbinden(pfad, ...teile) {
20
+ const wsl = wslUncZerlegen(pfad);
21
+ return wsl !== undefined ? path.posix.join(wsl.linuxPfad, ...teile) : path.join(pfad, ...teile);
22
+ }
15
23
  /**
16
24
  * Ob eine Adresse auf dieser Plattform bedienbar ist (E78, Entscheidung 7). `wsl:`-Heime gibt es
17
25
  * nur unter Windows; steht eines im Zustand, weil er von einem Windows-Rechner stammt, wird sein
@@ -6,7 +6,8 @@ import path from 'node:path';
6
6
  import { Document, isMap, isScalar, isSeq, parseDocument } from 'yaml';
7
7
  import { McpAbweichenderEintrag, OrtNichtUnterstuetzt, SkillOrdnerUngueltig, } from "../kern/modell.js";
8
8
  import { gitignoreEintragen, istGitRepo } from "./gitignore.js";
9
- import { nurHome } from "./heime.js";
9
+ import { LokalerZugriff } from "./dateisystem.js";
10
+ import { arbeitsverzeichnisVerbinden, nurHome } from "./heime.js";
10
11
  import { tiefGleich } from "./vergleich.js";
11
12
  const HERMES_VERZEICHNIS = '.hermes';
12
13
  const PROFILE_VERZEICHNIS = 'profiles';
@@ -70,6 +71,8 @@ function mcpKonfigAlsObjekt(konfig) {
70
71
  export class Hermes {
71
72
  name = 'hermes';
72
73
  heime;
74
+ /** Zugriff je Ort (E96): lokal, für ein Arbeitsverzeichnis in WSL die Bridge der Distro. */
75
+ zugriffFuer;
73
76
  /**
74
77
  * MCP-Server (E61): nur Home und Profil, denn Hermes kennt keine Projektebene für MCPs. Bindung
75
78
  * über Arrow-Funktionen auf `this`, damit die Fähigkeit ohne eigene Klasse auskommt.
@@ -86,8 +89,10 @@ export class Hermes {
86
89
  * Hermes' Zielverzeichnis steht in einer config.yaml; die über die Bridge zu lesen und zu
87
90
  * schreiben wäre ein eigener Schnitt. Ein blanker Pfad steht für „genau dieses Home".
88
91
  */
89
- constructor(heime) {
92
+ constructor(heime, zugriffFuer) {
90
93
  this.heime = typeof heime === 'string' ? nurHome(heime) : heime;
94
+ const lokal = new LokalerZugriff();
95
+ this.zugriffFuer = zugriffFuer ?? (() => lokal);
91
96
  }
92
97
  get home() {
93
98
  return this.heime.primaer(this.name);
@@ -155,7 +160,7 @@ export class Hermes {
155
160
  }
156
161
  zielVerzeichnis(ort) {
157
162
  if (ort.art === 'arbeitsverzeichnis')
158
- return path.join(ort.pfad, HERMES_VERZEICHNIS, EIGENER_ORDNER);
163
+ return arbeitsverzeichnisVerbinden(ort.pfad, HERMES_VERZEICHNIS, EIGENER_ORDNER);
159
164
  const hermesHome = this.hermesHome(ort);
160
165
  return this.ersterEintrag(hermesHome) ?? path.join(hermesHome, STANDARD_ORDNER);
161
166
  }
@@ -246,13 +251,15 @@ export class Hermes {
246
251
  }
247
252
  async vorbereiten(ort) {
248
253
  const verzeichnis = this.zielVerzeichnis(ort);
249
- await mkdir(verzeichnis, { recursive: true });
250
254
  if (ort.art === 'arbeitsverzeichnis') {
255
+ // Über den Zugriff: in einem WSL-Arbeitsverzeichnis legt die Bridge der Distro es an (E96).
256
+ await this.zugriffFuer(ort).verzeichnisAnlegen(verzeichnis);
251
257
  // .hermes/ darf nicht ins Repo (B7).
252
258
  if (await istGitRepo(ort.pfad))
253
259
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
254
260
  return;
255
261
  }
262
+ await mkdir(verzeichnis, { recursive: true });
256
263
  await this.registrieren(this.hermesHome(ort), verzeichnis);
257
264
  }
258
265
  /** Trägt das Verzeichnis in skills.external_dirs ein, falls es fehlt. Kommentare bleiben erhalten. */
@@ -151,6 +151,9 @@ export class WslZugriff {
151
151
  constructor(bruecke) {
152
152
  this.bruecke = bruecke;
153
153
  }
154
+ linkFehlerHinweis() {
155
+ return `Prüfe, ob die Distro ${this.bruecke.distro} läuft und das Linkziel unter /mnt/c erreichbar ist.`;
156
+ }
154
157
  verbinden(verzeichnis, name) {
155
158
  return path.posix.join(verzeichnis, name);
156
159
  }
package/dist/cli/hilfe.js CHANGED
@@ -104,6 +104,11 @@ Plugins des Vorhabens stehen. Ohne Zeiger bleibt das Verzeichnis gemerkt und jed
104
104
  (E26). dir forget löst Bindung und Eintrag, die gelegten Links bleiben liegen. Gebunden wird nur für
105
105
  Harnesses, die hmgr verwaltet: standardmäßig nur claude-code (E38).
106
106
 
107
+ Ein Arbeitsverzeichnis unter Windows, das in einer WSL-Distro liegt (\\\\wsl$\\<distro>\\… oder
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.
111
+
107
112
  Schalter:
108
113
  --composer <pfad> nur bei add: composer.json wählen, wenn unter dem Verzeichnis mehrere in
109
114
  Frage kommen
@@ -623,6 +628,8 @@ Links dort legt hmgr über wsl.exe an; das Linkziel bleibt der vorhandene Window
623
628
  nach /mnt/c. MCP-Server und Plugins gibt es dort nicht: die claude-CLI dieses Rechners bedient nur das
624
629
  primäre Homedir. remove lässt hmgr das Homedir vergessen, die Links dort bleiben liegen.
625
630
 
631
+ Ein Arbeitsverzeichnis unter \\\\wsl$\\<distro>\\… bedient hmgr ebenso über die Distro (hmgr help dir, E96).
632
+
626
633
  Zusätzliche Homedirs kennt bisher nur claude-code, und nur unter Windows. Steht eines im Zustand,
627
634
  etwa weil er von einem Windows-Rechner kommt, zeigt hmgr sein Ziel auf anderen Plattformen nicht.
628
635
 
package/dist/cli/main.js CHANGED
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { Anwendungsfaelle, } from "../kern/anwendungsfaelle.js";
8
- import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimOhneCliZugriff, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
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";
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";
@@ -53,13 +53,14 @@ export function bauen(umgebung, optionen = {}) {
53
53
  };
54
54
  const lokal = new LokalerZugriff();
55
55
  const zugriffFuerOrt = (ort) => {
56
- const adresse = ort.art === 'home' ? ort.heim : undefined;
57
- const zerlegt = adresse === undefined ? undefined : heimAdresseZerlegen(adresse);
58
- return zerlegt === undefined ? lokal : new WslZugriff(bruecke(zerlegt.distro));
56
+ // Ein WSL-Heim (E78) und ein Arbeitsverzeichnis unter \\wsl$\<distro> (E96) gehen über die
57
+ // Bridge ihrer Distro; alles andere über das lokale Dateisystem.
58
+ const distro = wslDistroVonOrt(ort);
59
+ return distro === undefined ? lokal : new WslZugriff(bruecke(distro));
59
60
  };
60
61
  const claude = new ClaudeCode(heime, optionen.claudeAusfuehrer ?? claudeAusfuehrer, zugriffFuerOrt);
61
62
  const ports = {
62
- harnesses: [claude, new Hermes(heime), new Agents(heime)],
63
+ harnesses: [claude, new Hermes(heime, zugriffFuerOrt), new Agents(heime, zugriffFuerOrt)],
63
64
  quellen: new GitQuellen(wurzel),
64
65
  ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome, (ziel) => zugriffFuerOrt(ziel.ort)),
65
66
  zeiger: new ComposerZeiger(),
@@ -1152,6 +1153,7 @@ function istModellFehler(fehler) {
1152
1153
  fehler instanceof HeimFaehigkeitFehlt ||
1153
1154
  fehler instanceof HeimNurUnterWindows ||
1154
1155
  fehler instanceof HeimOhneCliZugriff ||
1156
+ fehler instanceof ArbeitsverzeichnisOhneCliZugriff ||
1155
1157
  fehler instanceof HeimUnbekannt ||
1156
1158
  fehler instanceof WslBrueckeFehlgeschlagen);
1157
1159
  }
@@ -1204,6 +1206,8 @@ function naechsterSchritt(fehler) {
1204
1206
  return `hmgr harness home ${fehler.harness} zeigt die eingetragenen Homedirs.`;
1205
1207
  if (fehler instanceof HeimUnbekannt)
1206
1208
  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>.';
1207
1211
  if (fehler instanceof HeimOhneCliZugriff)
1208
1212
  return 'Skill-Links dort legt hmgr selbst: hmgr link <skill> --harness claude-code --ort <adresse>.';
1209
1213
  if (fehler instanceof WslBrueckeFehlgeschlagen)
@@ -1,8 +1,44 @@
1
1
  // Die installierte hmgr-CLI aus Sicht der GUI (E77). Die GUI trägt ihre eigene Version in sich
2
2
  // (`paketVersion()`, E72) — die CLI daneben ist ein zweites, unabhängig installiertes Paket, ihre
3
3
  // Version steht nur über ihren eigenen Aufruf fest.
4
+ import { homedir } from 'node:os';
5
+ import path from 'node:path';
4
6
  import spawn from 'cross-spawn';
5
7
  import { versionLesen } from "../kern/version.js";
8
+ /**
9
+ * Zeigt der aufgelöste Treffer auf die laufende GUI selbst (E97)? Deren Electron-exe heißt ebenfalls
10
+ * `hmgr.exe`; eine zweite Instanz endet an der Einzelinstanz-Sperre ohne Ausgabe und würde als
11
+ * „CLI vor 0.0.7" gelten. Unter win32 ohne Rücksicht auf Groß-/Kleinschreibung.
12
+ */
13
+ export function istEigeneGui(aufgeloest, execPath, plattform) {
14
+ if (!aufgeloest)
15
+ return false;
16
+ const a = path.resolve(aufgeloest);
17
+ const b = path.resolve(execPath);
18
+ return plattform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
19
+ }
20
+ /**
21
+ * Spawn-Optionen der CLI-Abfrage. Festes `cwd` (E97): `which` sucht unter Windows zuerst im
22
+ * Arbeitsverzeichnis, und cross-spawn wechselt für die Suche in genau dieses `cwd` — läge es im
23
+ * Installationsordner der GUI, würde die eigene `hmgr.exe` statt der CLI gefunden.
24
+ */
25
+ export function cliSpawnOptionen(heim = homedir()) {
26
+ return {
27
+ stdio: ['ignore', 'pipe', 'ignore'],
28
+ timeout: 10_000,
29
+ killSignal: 'SIGKILL',
30
+ cwd: heim,
31
+ };
32
+ }
33
+ /**
34
+ * Der Pfad, den cross-spawn für `hmgr` starten würde. `_parse` setzt `file` nur unter win32 (in
35
+ * parse.js `parseNonShell`, nur dort wird aufgelöst); sonst bleibt es `undefined`. `file` ist der
36
+ * aufgelöste Pfad vor einer Umsetzung auf cmd.exe oder Shebang, also der Treffer von `which`.
37
+ */
38
+ function aufgeloesterPfad(optionen) {
39
+ const geparst = spawn._parse('hmgr', ['--version'], optionen);
40
+ return geparst.file;
41
+ }
6
42
  /**
7
43
  * `hmgr --version` (E72: nur die Nummer, Exit 0). Kein Fehler dringt nach außen — der Aufrufer
8
44
  * entscheidet anhand des Befunds, was zu melden ist.
@@ -22,11 +58,11 @@ export function hmgrCliVersion() {
22
58
  aufloesen(wert);
23
59
  };
24
60
  try {
25
- const kind = spawn('hmgr', ['--version'], {
26
- stdio: ['ignore', 'pipe', 'ignore'],
27
- timeout: 10_000,
28
- killSignal: 'SIGKILL',
29
- });
61
+ const optionen = cliSpawnOptionen();
62
+ if (istEigeneGui(aufgeloesterPfad(optionen), process.execPath, process.platform)) {
63
+ return melden({ art: 'fehlt' });
64
+ }
65
+ const kind = spawn('hmgr', ['--version'], optionen);
30
66
  let stdout = '';
31
67
  kind.stdout.on('data', (stueck) => (stdout += stueck));
32
68
  kind.on('error', () => melden({ art: 'fehlt' }));
package/dist/gui/main.js CHANGED
@@ -886,6 +886,35 @@ function fensterOeffnen() {
886
886
  } catch (fehler) {
887
887
  bundleZerbrichtText = String(fehler);
888
888
  }
889
+ // Sammelstreifen unter „Alle" (E95): das Bundle „vorhaben" liegt an kunde-x in Claude Code
890
+ // und Hermes — ein Streifen mit beiden Harness-Zeichen, Entfernen nennt beide Harnesses
891
+ // (nur der Dialogtext, kein Klick auf „Ja"). Unter dem Harness-Chip steht weiter die
892
+ // volle Gruppe mit Zeilen; danach zurück auf „Alle".
893
+ var streifenKarte = Array.from(document.querySelectorAll('#zielliste .ziel.verzeichnis'))
894
+ .find((k) => (k.querySelector('.zielkopf')?.textContent || '').indexOf('kunde-x') > -1);
895
+ var streifenZahl = null, streifenHarnesse = null, streifenText = null;
896
+ var streifenDialogTitel = null, streifenDialogText = null;
897
+ var chipGruppeZahl = null, chipGruppeZeilenZahl = null, chipStreifenZahl = null;
898
+ try {
899
+ var streifen = Array.from(streifenKarte?.querySelectorAll('.bundlegruppe') ?? []);
900
+ streifenZahl = streifen.length;
901
+ streifenHarnesse = streifen[0]?.querySelector('.zeilenzeichen')?.getAttribute('aria-label') ?? null;
902
+ streifenText = streifen[0]?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
903
+ streifen[0]?.querySelector('button')?.click();
904
+ streifenDialogTitel = document.getElementById('dx-titel')?.textContent ?? null;
905
+ streifenDialogText = document.getElementById('dx-text')?.textContent ?? null;
906
+ document.querySelector('#d-entfernen button[value="nein"]')?.click();
907
+ var chipClaude = streifenKarte?.querySelector('.kartenchips [data-chipwahl="claude-code"]');
908
+ chipClaude?.click();
909
+ var kundeXNachChip = Array.from(document.querySelectorAll('#zielliste .ziel.verzeichnis'))
910
+ .find((k) => (k.querySelector('.zielkopf')?.textContent || '').indexOf('kunde-x') > -1);
911
+ chipGruppeZahl = kundeXNachChip?.querySelectorAll('.bundlegruppe').length ?? null;
912
+ chipGruppeZeilenZahl = kundeXNachChip?.querySelectorAll('.bundlegruppe .linkliste li').length ?? null;
913
+ chipStreifenZahl = kundeXNachChip?.querySelectorAll('.bundlegruppe .zeilenzeichen').length ?? null;
914
+ kundeXNachChip?.querySelector('.kartenchips [data-chipwahl="alle"]')?.click();
915
+ } catch (fehler) {
916
+ streifenDialogText = String(fehler);
917
+ }
889
918
  // MCP-Ansicht (E61-Folgerunde, F46: abgespeckt zum reinen Katalog): Navigationsreihenfolge
890
919
  // Ziele, MCP, Plugins, Skills, Karten mit Name/Beschreibung/Homepage-Link, F39-Warnzeile,
891
920
  // Einhängen-Dialog öffnen/abbrechen ohne IPC-Schreibaktion. Orte-Liste und Fremdkasten
@@ -1172,6 +1201,14 @@ function fensterOeffnen() {
1172
1201
  bundleGruppeText,
1173
1202
  bundleGruppeZeilen,
1174
1203
  bundleZerbrichtText,
1204
+ streifenZahl,
1205
+ streifenHarnesse,
1206
+ streifenText,
1207
+ streifenDialogTitel,
1208
+ streifenDialogText,
1209
+ chipGruppeZahl,
1210
+ chipGruppeZeilenZahl,
1211
+ chipStreifenZahl,
1175
1212
  interGeladen: document.fonts.check('400 16px Inter'),
1176
1213
  monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
1177
1214
  quelleneuVorhanden,
@@ -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.</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. Arbeitsverzeichnisse in WSL (\\wsl$\…) gehen dort nicht: die claude-CLI dieses Rechners kennt ihre Konfiguration nicht.</p>
146
146
  </div>
147
147
 
148
148
  <div class="mcpliste" id="mcpliste"></div>
@@ -345,7 +345,7 @@
345
345
  <div class="dkopf">
346
346
  <span class="etikett">Homedir in WSL</span>
347
347
  <h3>Welche Distro?</h3>
348
- <p>Claude Code in einer WSL-Distro hat ein eigenes Homedir. hmgr ermittelt es einmal, zeigt es an und legt Links dort über wsl.exe an; die Checkouts bleiben, wo sie sind. Das Ziel steht danach als eigene Karte neben dem Windows-Homedir, gespiegelt wird nichts.</p>
348
+ <p>Claude Code in einer WSL-Distro hat ein eigenes Homedir. hmgr ermittelt es einmal, zeigt es an und legt Links dort über wsl.exe an; die Checkouts bleiben, wo sie sind. Das Ziel steht danach als eigene Karte neben dem Windows-Homedir, gespiegelt wird nichts. Ein Arbeitsverzeichnis unter \\wsl$\… bedient hmgr ebenfalls über die Distro, ohne dass du hier etwas eintragen musst.</p>
349
349
  </div>
350
350
  <div class="dkoerper">
351
351
  <div class="feld">
@@ -826,21 +826,35 @@
826
826
  return gruppe;
827
827
  }
828
828
 
829
- /** Ein Bundle als einzelner Streifen (Chip „Alle" einer Verzeichnis-Karte, E69/E53): Harness-
830
- Zeichen, Name, seine Mitglieder als Text, Entfernen. Ohne eigene Tabelle. */
831
- function bundleStreifen(b, z) {
829
+ /** Ein Bundle als einzelner Streifen (Chip „Alle" einer Verzeichnis-Karte, E69/E53/E95): die
830
+ Harness-Zeichen aller Harnesses, in denen es liegt (wie in den Skill-Zeilen), Name, Mitglieder,
831
+ Entfernen für alle diese Harnesses. `treffer`: [{b, z}], je Harness einer, gleicher Name und
832
+ gleiche Quelle. Mitglieder: die des ersten Treffers, abweichende anderer Harnesses folgen. */
833
+ function bundleStreifen(treffer) {
834
+ var b = treffer[0].b;
832
835
  var streifen = e('div', 'bundlegruppe');
833
836
  var kopf = e('div', 'bundlekopf');
834
837
  var titel = e('span', 'bundletitel');
835
- titel.appendChild(harnessZeichen(z.ziel.harness));
838
+ var harnesse = [];
839
+ treffer.forEach(function (t) { if (harnesse.indexOf(t.z.ziel.harness) === -1) harnesse.push(t.z.ziel.harness); });
840
+ harnesse.sort(function (x, y) { return harnessName(x).localeCompare(harnessName(y), 'de'); });
841
+ titel.appendChild(harnesseZeichenSammel(harnesse));
836
842
  titel.appendChild(e('span', 'etikett', 'Bundle'));
837
843
  titel.appendChild(e('b', 'bundlename', b.name));
838
844
  kopf.appendChild(titel);
839
- kopf.appendChild(e('span', 'bundlemitglieder', b.skills.join(', ')));
845
+ var mitglieder = [];
846
+ treffer.forEach(function (t) {
847
+ t.b.skills.forEach(function (n) { if (mitglieder.indexOf(n) === -1) mitglieder.push(n); });
848
+ });
849
+ kopf.appendChild(e('span', 'bundlemitglieder', mitglieder.join(', ')));
840
850
  var weg = e('button', 'knopf leise klein', 'Entfernen');
841
851
  weg.type = 'button';
842
- weg.setAttribute('aria-label', 'Bundle ' + b.name + ' aus ' + zielTitel(z) + ' entfernen');
843
- weg.addEventListener('click', function () { frageBundleEntfernen(b, z.ziel); });
852
+ weg.setAttribute('aria-label', 'Bundle ' + b.name + ' aus '
853
+ + treffer.map(function (t) { return zielTitel(t.z); }).join(', ') + ' entfernen');
854
+ weg.addEventListener('click', function () {
855
+ if (treffer.length === 1) frageBundleEntfernen(b, treffer[0].z.ziel);
856
+ else frageBundleEntfernenAlle(b.name, mitglieder, treffer);
857
+ });
844
858
  kopf.appendChild(weg);
845
859
  streifen.appendChild(kopf);
846
860
  return streifen;
@@ -1209,13 +1223,22 @@
1209
1223
  // Bundle gehört aber zu genau einem Harness — deshalb hier ein Streifen je Bundle mit seinem
1210
1224
  // Harness-Zeichen statt einer Umgruppierung der Tabelle. Unter dem Harness-Chip zeigt linkTeil
1211
1225
  // die volle Gruppe über den Zeilen.
1226
+ // Ein Bundle, das in mehreren Harnesses am selben Ort liegt, ist hier ein Streifen (E95), wie
1227
+ // die Skill-Zeilen: Schlüssel ist Name plus Quelle, gleichnamige Bundles aus zwei Quellen
1228
+ // bleiben getrennt.
1229
+ var bundleGruppen = {}, bundleReihenfolge = [];
1212
1230
  teile.forEach(function (z) {
1213
1231
  (z.bundles || [])
1214
1232
  .filter(function (b) { return b.skills.length > 0; })
1215
- .slice()
1216
- .sort(function (a, b) { return a.name.localeCompare(b.name, 'de'); })
1217
- .forEach(function (b) { teil.appendChild(bundleStreifen(b, z)); });
1233
+ .forEach(function (b) {
1234
+ var schluessel = b.name + '\u0000' + b.quelleText;
1235
+ if (!bundleGruppen[schluessel]) { bundleGruppen[schluessel] = []; bundleReihenfolge.push(schluessel); }
1236
+ bundleGruppen[schluessel].push({ b: b, z: z });
1237
+ });
1218
1238
  });
1239
+ bundleReihenfolge
1240
+ .sort(function (a, b) { return bundleGruppen[a][0].b.name.localeCompare(bundleGruppen[b][0].b.name, 'de'); })
1241
+ .forEach(function (k) { teil.appendChild(bundleStreifen(bundleGruppen[k])); });
1219
1242
 
1220
1243
  var kopf = e('div', 'linkkopf');
1221
1244
  kopf.appendChild(e('span'));
@@ -1612,12 +1635,24 @@
1612
1635
 
1613
1636
  /** Entfernen des ganzen Bundles an einem Ziel (E69): alle seine Links auf einmal. */
1614
1637
  function frageBundleEntfernen(b, ziel) {
1615
- entfernenKontext = { art: 'bundle', bundle: b.name, ziel: ziel };
1638
+ entfernenKontext = { art: 'bundle', bundle: b.name, ziele: [ziel] };
1616
1639
  $('dx-titel').textContent = 'Bundle ' + b.name + ' entfernen?';
1617
1640
  $('dx-text').textContent = zahlwort(b.skills.length, 'Link', 'Links') + ' aus ' + zielTitel({ ziel: ziel })
1618
1641
  + ' verschwinden: ' + b.skills.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
1619
1642
  zeige($('d-entfernen'));
1620
1643
  }
1644
+ /** Entfernen eines Bundles in allen Harnesses seines Streifens (E95): `treffer` wie bei
1645
+ bundleStreifen, der Dialog nennt die Harnesses mit ihrer Linkzahl und die Mitglieder. */
1646
+ function frageBundleEntfernenAlle(name, mitglieder, treffer) {
1647
+ entfernenKontext = { art: 'bundle', bundle: name, ziele: treffer.map(function (t) { return t.z.ziel; }) };
1648
+ var proHarness = treffer.map(function (t) {
1649
+ return harnessName(t.z.ziel.harness) + ' ' + zahlwort(t.b.skills.length, 'Link', 'Links');
1650
+ });
1651
+ $('dx-titel').textContent = 'Bundle ' + name + ' entfernen?';
1652
+ $('dx-text').textContent = 'Das Bundle verschwindet in ' + treffer.length + ' Harnesses (' + proHarness.join(', ')
1653
+ + '): ' + mitglieder.join(', ') + '. Die Checkouts bleiben liegen, bis kein Link mehr darauf zeigt.';
1654
+ zeige($('d-entfernen'));
1655
+ }
1621
1656
  /** Entfernen einer „Alle"-Zeile (E53 Pkt. 2): entfernbareZiele sind die Ziele (voller z.ziel), in
1622
1657
  denen der Link weg soll, nichtEntfernbareHarnesse die, in denen er nur über ein Plugin hängt
1623
1658
  und liegen bleibt. Der Dialog nennt beides. */
@@ -1640,17 +1675,29 @@
1640
1675
  nachDialog($('d-entfernen'), async function (wert) {
1641
1676
  if (wert !== 'ja' || entfernenKontext === null) return;
1642
1677
  var kontext = entfernenKontext;
1643
- // Bundle als Ganzes (E69): ein Aufruf, der Kern nimmt alle Links des Bundles weg.
1678
+ // Bundle als Ganzes (E69): je Ziel ein Aufruf, der Kern nimmt alle Links des Bundles weg. Bei
1679
+ // mehreren Zielen (E95) hält ein Fehlschlag die übrigen nicht auf, rufe meldet ihn selbst.
1644
1680
  if (kontext.art === 'bundle') {
1645
- var wegB = await rufe(function () {
1646
- return window.hmgr.bundleEntfernen({
1647
- bundle: kontext.bundle,
1648
- harness: kontext.ziel.harness,
1649
- ort: kontext.ziel.ort,
1650
- });
1651
- });
1681
+ var entferntB = 0;
1682
+ var gescheitertB = [];
1683
+ for (var j = 0; j < kontext.ziele.length; j++) {
1684
+ var zielB = kontext.ziele[j];
1685
+ try {
1686
+ var wegB = await rufe(function () {
1687
+ return window.hmgr.bundleEntfernen({ bundle: kontext.bundle, harness: zielB.harness, ort: zielB.ort });
1688
+ });
1689
+ entferntB += wegB.entfernt.length;
1690
+ } catch (fehler) {
1691
+ gescheitertB.push(harnessName(zielB.harness));
1692
+ }
1693
+ }
1652
1694
  await ladeAlles();
1653
- melde('Bundle ' + kontext.bundle + ' entfernt, ' + zahlwort(wegB.entfernt.length, 'Link', 'Links') + ' weniger.');
1695
+ if (gescheitertB.length === 0) {
1696
+ melde('Bundle ' + kontext.bundle + ' entfernt, ' + zahlwort(entferntB, 'Link', 'Links') + ' weniger.');
1697
+ } else if (kontext.ziele.length > 1) {
1698
+ melde('Bundle ' + kontext.bundle + ' blieb in ' + gescheitertB.join(', ') + ' liegen, '
1699
+ + zahlwort(entferntB, 'Link', 'Links') + ' weniger.');
1700
+ }
1654
1701
  return;
1655
1702
  }
1656
1703
  // Ein Fehlschlag in einem Harness hält die übrigen nicht auf: rufe hat ihn schon gemeldet, die
@@ -1,6 +1,6 @@
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, 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, 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";
@@ -471,9 +471,11 @@ export class Anwendungsfaelle {
471
471
  if (ebenen.includes('profil') && kannProfile(harness))
472
472
  for (const name of await harness.profile())
473
473
  ziele.push({ harness: harness.name, ort: { art: 'profil', name } });
474
+ // Arbeitsverzeichnisse in WSL (E96) ebenso nicht: ihre Konfiguration liegt in der Distro.
474
475
  if (ebenen.includes('arbeitsverzeichnis'))
475
476
  for (const pfad of zustand.arbeitsverzeichnisse)
476
- ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
477
+ if (wslUncZerlegen(pfad) === undefined)
478
+ ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
477
479
  }
478
480
  return ziele;
479
481
  }
@@ -2363,16 +2365,16 @@ export class Anwendungsfaelle {
2363
2365
  }
2364
2366
  // Installierte Plugins je Ziel vorab, mit begrenzter Parallelität (E93): `claude plugin list`
2365
2367
  // ist ein Prozess je Ziel (cwd-gebunden, kein Sammelaufruf), nacheinander dauert das bei vielen
2366
- // Arbeitsverzeichnissen Sekunden. Plugins nur am primären Homedir (E78): die claude-CLI dieses
2367
- // Rechners kennt die Plugin-Konfiguration einer WSL-Distro nicht, ihre user-Liste gehörte zum
2368
- // Windows-Home.
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.
2369
2371
  const pluginListen = new Map();
2370
2372
  if (mitPlugins) {
2371
2373
  const abfragen = [];
2372
2374
  for (const { harness, ziele } of plan)
2373
2375
  if (kannPlugins(harness))
2374
2376
  for (const ziel of ziele)
2375
- if (!(ziel.ort.art === 'home' && ziel.ort.heim !== undefined))
2377
+ if (wslDistroVonOrt(ziel.ort) === undefined)
2376
2378
  abfragen.push({
2377
2379
  schluessel: zielSchluessel(ziel),
2378
2380
  abruf: () => harness.plugins.installierte(ziel).catch(() => []),
@@ -68,6 +68,29 @@ export function heimAdresseZerlegen(text) {
68
68
  return undefined;
69
69
  return { art: 'wsl', distro };
70
70
  }
71
+ /**
72
+ * Zerlegt einen WSL-UNC-Pfad (E96): `\\wsl$\<distro>\…` oder `\\wsl.localhost\<distro>\…`, auch mit
73
+ * `\\?\UNC\`-Präfix und mit Schrägstrichen in beiden Richtungen. Der Hostteil gilt ohne Rücksicht
74
+ * auf Groß-/Kleinschreibung, der Distro-Name bleibt unverändert. `linuxPfad` ist der Pfad in der
75
+ * Distro, absolut und ohne abschließenden Trenner. Alles andere ergibt undefined. Rein und
76
+ * plattformunabhängig, damit auch Tests außerhalb von Windows ihn abdecken.
77
+ */
78
+ export function wslUncZerlegen(pfad) {
79
+ const treffer = /^\\\\(?:\?\\UNC\\)?(?:wsl\$|wsl\.localhost)\\([^\\]+)(?:\\(.*))?$/i.exec(pfad.trim().replaceAll('/', '\\'));
80
+ if (treffer === null)
81
+ return undefined;
82
+ const distro = treffer[1];
83
+ const rest = (treffer[2] ?? '').split('\\').filter((teil) => teil.length > 0);
84
+ return { distro, linuxPfad: `/${rest.join('/')}` };
85
+ }
86
+ /** Die WSL-Distro, über deren Bridge ein Ort bedient wird: ein `wsl:`-Heim (E78) oder ein WSL-UNC-Arbeitsverzeichnis (E96). */
87
+ export function wslDistroVonOrt(ort) {
88
+ if (ort.art === 'home')
89
+ return ort.heim === undefined ? undefined : heimAdresseZerlegen(ort.heim)?.distro;
90
+ if (ort.art === 'arbeitsverzeichnis')
91
+ return wslUncZerlegen(ort.pfad)?.distro;
92
+ return undefined;
93
+ }
71
94
  /** Die Adresse eines zusätzlichen Heims in der Schreibweise, die hmgr speichert und anzeigt. */
72
95
  export function heimAdresseAlsText(adresse) {
73
96
  return `wsl:${adresse.distro}`;
@@ -137,6 +160,22 @@ export class HeimOhneCliZugriff extends Error {
137
160
  this.adresse = adresse;
138
161
  }
139
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;
177
+ }
178
+ }
140
179
  /**
141
180
  * Die Bridge in eine WSL-Distro hat nicht geantwortet (E78): die Distro gibt es nicht, sie startet
142
181
  * nicht, oder wsl.exe fehlt. Die Ausgabe von wsl.exe steht mit in der Meldung — sie nennt den Grund
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",