hmgr 0.0.29 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/claude-code.js +254 -78
- package/dist/adapter/git.js +36 -1
- package/dist/adapter/wsl.js +83 -13
- package/dist/cli/hilfe.js +37 -8
- package/dist/cli/main.js +40 -11
- package/dist/gui/renderer/index.html +2 -2
- package/dist/kern/anwendungsfaelle.js +57 -26
- package/dist/kern/modell.js +36 -27
- package/package.json +1 -1
|
@@ -3,33 +3,23 @@ import spawn from 'cross-spawn';
|
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { readFile, stat } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
function ohneCliZugriffPruefen(harness, ort, was) {
|
|
28
|
-
const heim = zusaetzlichesHeim(ort);
|
|
29
|
-
if (heim !== undefined)
|
|
30
|
-
throw new HeimOhneCliZugriff(harness, heim, was);
|
|
31
|
-
if (ort.art === 'arbeitsverzeichnis' && wslUncZerlegen(ort.pfad) !== undefined)
|
|
32
|
-
throw new ArbeitsverzeichnisOhneCliZugriff(harness, ort.pfad, was);
|
|
20
|
+
/** Der Ort, unter dem `ClaudeCode` die claude-CLI einer Distro anspricht: ihr Heim. */
|
|
21
|
+
function distroOrt(distro) {
|
|
22
|
+
return { art: 'home', heim: `wsl:${distro}` };
|
|
33
23
|
}
|
|
34
24
|
const GITIGNORE_ZEILE = '.claude/';
|
|
35
25
|
/**
|
|
@@ -60,6 +50,21 @@ export const claudeAusfuehrer = (args, cwd) => new Promise((aufloesen) => {
|
|
|
60
50
|
kind.on('error', (fehler) => aufloesen({ stdout: fehler.message, code: 1 }));
|
|
61
51
|
kind.on('close', (code) => aufloesen({ stdout: code === 0 ? stdout : `${stdout}${stderr}`, code: code ?? 1 }));
|
|
62
52
|
});
|
|
53
|
+
/**
|
|
54
|
+
* Pfadvergleich für E102: abschließende Trenner zählen nicht; bei Windows-Pfaden (Laufwerk oder
|
|
55
|
+
* UNC) sind außerdem `\\` und `/` sowie Groß-/Kleinschreibung gleich.
|
|
56
|
+
*/
|
|
57
|
+
export function pfadGleich(a, b) {
|
|
58
|
+
const windows = (p) => /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith('\\\\');
|
|
59
|
+
const norm = (p, win) => {
|
|
60
|
+
let t = win ? p.replaceAll('\\', '/').toLowerCase() : p;
|
|
61
|
+
while (t.length > 1 && t.endsWith('/'))
|
|
62
|
+
t = t.slice(0, -1);
|
|
63
|
+
return t;
|
|
64
|
+
};
|
|
65
|
+
const win = windows(a) || windows(b);
|
|
66
|
+
return norm(a, win) === norm(b, win);
|
|
67
|
+
}
|
|
63
68
|
export class PluginListeUnbekannteForm extends Error {
|
|
64
69
|
constructor(text) {
|
|
65
70
|
super(`claude plugin list --json: ${text}`);
|
|
@@ -96,7 +101,13 @@ export function pluginListeLesen(text) {
|
|
|
96
101
|
const scope = o['scope'];
|
|
97
102
|
if (!istScope(scope))
|
|
98
103
|
throw new PluginListeUnbekannteForm(`„${id}" hat keinen bekannten Scope`);
|
|
99
|
-
|
|
104
|
+
const projectPath = o['projectPath'];
|
|
105
|
+
return {
|
|
106
|
+
name: id.slice(0, trenner),
|
|
107
|
+
marketplace: id.slice(trenner + 1),
|
|
108
|
+
scope,
|
|
109
|
+
...(typeof projectPath === 'string' ? { projectPath } : {}),
|
|
110
|
+
};
|
|
100
111
|
});
|
|
101
112
|
}
|
|
102
113
|
/**
|
|
@@ -116,16 +127,25 @@ export function marketplaceListeLesen(text) {
|
|
|
116
127
|
/**
|
|
117
128
|
* Plugins über `claude plugin …`. Scope ist immer `local` mit dem Arbeitsverzeichnis selbst als
|
|
118
129
|
* cwd (E27). Nur das Home des Nutzers läuft als `--scope user` ohne cwd.
|
|
130
|
+
*
|
|
131
|
+
* Welche claude-CLI, entscheidet der Ort (E101, derselbe Weg wie MCP seit E100): ein WSL-Heim und
|
|
132
|
+
* ein Arbeitsverzeichnis unter `\\wsl$` gehen an die CLI der Distro, das cwd ist dort der
|
|
133
|
+
* Linux-Pfad. Jede Installation führt ihre eigene Marketplace-Liste, deshalb wirken auch die
|
|
134
|
+
* Marketplace-Operationen in der Welt des genannten Orts. Vor jedem Aufruf prüft die Fähigkeit, ob
|
|
135
|
+
* dort eine CLI antwortet, sonst `ClaudeCliNichtErreichbar`.
|
|
119
136
|
*/
|
|
120
137
|
class ClaudePlugins {
|
|
121
138
|
harness;
|
|
122
|
-
|
|
123
|
-
constructor(harness, ausfuehrer) {
|
|
139
|
+
constructor(harness) {
|
|
124
140
|
this.harness = harness;
|
|
125
|
-
this.ausfuehrer = ausfuehrer;
|
|
126
141
|
}
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
/** Die claude-CLI des Orts, nachdem feststeht, dass sie antwortet. */
|
|
143
|
+
async cli(ort) {
|
|
144
|
+
await this.harness.cliPruefen(ort);
|
|
145
|
+
return this.harness.ausfuehrerFuer(ort);
|
|
146
|
+
}
|
|
147
|
+
async rufen(ort, args, cwd) {
|
|
148
|
+
const { stdout, code } = await (await this.cli(ort))(args, cwd);
|
|
129
149
|
if (code !== 0)
|
|
130
150
|
throw new Error(`claude ${args.join(' ')} scheiterte (Code ${code}): ${stdout.trim()}`);
|
|
131
151
|
return stdout;
|
|
@@ -134,48 +154,80 @@ class ClaudePlugins {
|
|
|
134
154
|
* Scope und cwd eines Ortes (E27). Das cwd ist das Arbeitsverzeichnis selbst, nicht sein
|
|
135
155
|
* Zielverzeichnis: außerhalb eines Git-Repos legt Claude Code die `settings.local.json` im
|
|
136
156
|
* Startverzeichnis ab, mit dem Zielverzeichnis als cwd landete sie unter `.claude/skills/`.
|
|
157
|
+
* In WSL steht das cwd als Linux-Pfad da (E101).
|
|
137
158
|
*/
|
|
138
159
|
scope(ziel) {
|
|
139
|
-
ohneCliZugriffPruefen(this.harness.name, ziel.ort, 'Plugins');
|
|
140
160
|
if (ziel.ort.art === 'home')
|
|
141
161
|
return { scope: 'user' };
|
|
142
162
|
if (ziel.ort.art === 'arbeitsverzeichnis')
|
|
143
|
-
return { scope: 'local', cwd: ziel.ort.pfad };
|
|
163
|
+
return { scope: 'local', cwd: cliPfad(ziel.ort.pfad) };
|
|
144
164
|
throw new OrtNichtUnterstuetzt(this.harness.name, ziel.ort.art);
|
|
145
165
|
}
|
|
146
|
-
async marketplaceBekannt(name) {
|
|
147
|
-
const { stdout, code } = await this.
|
|
166
|
+
async marketplaceBekannt(name, ort) {
|
|
167
|
+
const { stdout, code } = await (await this.cli(ort))(['plugin', 'marketplace', 'list']);
|
|
148
168
|
if (code !== 0)
|
|
149
169
|
return false;
|
|
150
170
|
return marketplaceListeLesen(stdout).includes(name);
|
|
151
171
|
}
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Eine lokale Quelle steht mit ihrem Windows-Pfad da; die CLI einer Distro braucht ihn in deren
|
|
174
|
+
* Schreibweise (E101): `C:\…` unter der Automount-Wurzel, `\\wsl$\<dieselbe Distro>\…` als
|
|
175
|
+
* Linux-Pfad. URLs und alles andere gehen unverändert durch.
|
|
176
|
+
*/
|
|
177
|
+
async marketplaceHinzufuegen(_name, url, ort) {
|
|
178
|
+
await this.rufen(ort, ['plugin', 'marketplace', 'add', await this.urlInWelt(url, ort)]);
|
|
179
|
+
}
|
|
180
|
+
async urlInWelt(url, ort) {
|
|
181
|
+
const distro = wslDistroVonOrt(ort);
|
|
182
|
+
if (distro === undefined)
|
|
183
|
+
return url;
|
|
184
|
+
const unc = wslUncZerlegen(url);
|
|
185
|
+
if (unc !== undefined)
|
|
186
|
+
return unc.distro.toLowerCase() === distro.toLowerCase() ? unc.linuxPfad : url;
|
|
187
|
+
if (/^[a-zA-Z]:[\\/]/.test(url))
|
|
188
|
+
return await this.harness.bruecke(distro).vomHost(url);
|
|
189
|
+
return url;
|
|
154
190
|
}
|
|
155
191
|
async installieren(plugin, marketplace, ziel) {
|
|
156
192
|
const { scope, cwd } = this.scope(ziel);
|
|
157
|
-
await this.rufen(['plugin', 'install', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
|
|
193
|
+
await this.rufen(ziel.ort, ['plugin', 'install', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
|
|
158
194
|
}
|
|
159
195
|
async entfernen(plugin, marketplace, ziel) {
|
|
160
196
|
const { scope, cwd } = this.scope(ziel);
|
|
161
|
-
await this.rufen(['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
|
|
197
|
+
await this.rufen(ziel.ort, ['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
|
|
162
198
|
}
|
|
163
199
|
/** Ohne Namen aktualisiert die CLI alle Marketplaces. Je Plugin verlangt sie einen Namen, das übernimmt hmgr nicht (E14). */
|
|
164
|
-
async aktualisieren() {
|
|
165
|
-
await this.rufen(['plugin', 'marketplace', 'update']);
|
|
200
|
+
async aktualisieren(ort) {
|
|
201
|
+
await this.rufen(ort, ['plugin', 'marketplace', 'update']);
|
|
202
|
+
}
|
|
203
|
+
async welten() {
|
|
204
|
+
return await this.harness.erreichbareWelten();
|
|
166
205
|
}
|
|
167
206
|
/**
|
|
168
207
|
* Die Liste gilt nur für das cwd des Aufrufs und enthält dort alle Scopes. Zum Ort gehören im
|
|
169
208
|
* Home die Plugins mit Scope `user`, im Arbeitsverzeichnis die mit `local` und `project`.
|
|
209
|
+
* E102: Die CLI listet die local-Einträge aller Projekte, je mit `projectPath`; am
|
|
210
|
+
* Arbeitsverzeichnis zählen nur die eigenen (ohne `projectPath` wie bisher). Verglichen wird mit
|
|
211
|
+
* dem CLI-Pfad (in WSL der Linux-Pfad) und lokal zusätzlich mit der aufgelösten Form, weil
|
|
212
|
+
* Claude Code den cwd auflöst.
|
|
170
213
|
*/
|
|
171
214
|
async installierte(ziel) {
|
|
172
215
|
const { cwd } = this.scope(ziel);
|
|
173
|
-
const { stdout, code } = await this.
|
|
216
|
+
const { stdout, code } = await (await this.cli(ziel.ort))(['plugin', 'list', '--json'], cwd);
|
|
174
217
|
if (code !== 0)
|
|
175
218
|
return [];
|
|
176
219
|
const erlaubt = ziel.ort.art === 'home' ? ['user'] : ['local', 'project'];
|
|
220
|
+
const eigene = [];
|
|
221
|
+
if (ziel.ort.art === 'arbeitsverzeichnis') {
|
|
222
|
+
eigene.push(cwd ?? ziel.ort.pfad);
|
|
223
|
+
if (wslUncZerlegen(ziel.ort.pfad) === undefined)
|
|
224
|
+
eigene.push(aufgeloest(ziel.ort.pfad));
|
|
225
|
+
}
|
|
177
226
|
return pluginListeLesen(stdout)
|
|
178
227
|
.filter((eintrag) => erlaubt.includes(eintrag.scope))
|
|
228
|
+
.filter((eintrag) => ziel.ort.art !== 'arbeitsverzeichnis' ||
|
|
229
|
+
eintrag.projectPath === undefined ||
|
|
230
|
+
eigene.some((pfad) => pfadGleich(pfad, eintrag.projectPath)))
|
|
179
231
|
.map((eintrag) => pluginSchluessel(eintrag));
|
|
180
232
|
}
|
|
181
233
|
}
|
|
@@ -222,8 +274,12 @@ function aufgeloest(pfad) {
|
|
|
222
274
|
* Windows angelegte Datei leer.
|
|
223
275
|
*/
|
|
224
276
|
async function projektMcpJsonLesen(verzeichnis) {
|
|
277
|
+
const text = await readFile(path.join(verzeichnis, '.mcp.json'), 'utf8').catch(() => undefined);
|
|
278
|
+
return text === undefined ? {} : mcpServersAusText(text);
|
|
279
|
+
}
|
|
280
|
+
/** `mcpServers` aus dem Text einer `.mcp.json`; kaputtes JSON heißt nichts, eine BOM stört nicht. */
|
|
281
|
+
function mcpServersAusText(text) {
|
|
225
282
|
try {
|
|
226
|
-
const text = await readFile(path.join(verzeichnis, '.mcp.json'), 'utf8');
|
|
227
283
|
return mcpServersVon(JSON.parse(ohneBom(text)));
|
|
228
284
|
}
|
|
229
285
|
catch {
|
|
@@ -239,17 +295,43 @@ async function projektMcpJsonLesen(verzeichnis) {
|
|
|
239
295
|
* geschrieben wird sie einzig von `geteiltenUebernehmen` auf ausdrücklichen Auftrag (E64). Format
|
|
240
296
|
* und Verhalten der CLI stehen in HARNESS-FAKTEN.md, geprüft an Claude Code 2.1.261 gegen ein
|
|
241
297
|
* eigenes CLAUDE_CONFIG_DIR, nie an einer echten Nutzerdatei.
|
|
298
|
+
*
|
|
299
|
+
* Welche CLI und welche Dateien, entscheidet der Ort (E100): ein WSL-Heim und ein Arbeitsverzeichnis
|
|
300
|
+
* unter `\\wsl$` gehen an die claude-CLI der Distro (cwd als Linux-Pfad), und gelesen werden
|
|
301
|
+
* `~/.claude.json` und `.mcp.json` der Distro über die Bridge — die Windows-Dateien wären die
|
|
302
|
+
* falschen. Vor jedem schreibenden Aufruf prüft die Fähigkeit, ob am Ort eine CLI antwortet;
|
|
303
|
+
* sonst hielte das idempotente Aushängen ein „nicht gefunden" der fehlenden CLI für Erfolg.
|
|
242
304
|
*/
|
|
243
305
|
class ClaudeMcp {
|
|
244
306
|
harness;
|
|
245
|
-
ausfuehrer;
|
|
246
307
|
/** Das primäre Homedir, jedes Mal frisch: es lässt sich zur Laufzeit umstellen (E78, F89). */
|
|
247
308
|
home;
|
|
248
|
-
constructor(harness,
|
|
309
|
+
constructor(harness, home) {
|
|
249
310
|
this.harness = harness;
|
|
250
|
-
this.ausfuehrer = ausfuehrer;
|
|
251
311
|
this.home = home;
|
|
252
312
|
}
|
|
313
|
+
/** Die claude-CLI des Orts, nachdem feststeht, dass sie antwortet (E100). */
|
|
314
|
+
async cli(ort) {
|
|
315
|
+
await this.harness.cliPruefen(ort);
|
|
316
|
+
return this.harness.ausfuehrerFuer(ort);
|
|
317
|
+
}
|
|
318
|
+
/** Der Pfad eines Arbeitsverzeichnisses in der Welt seiner CLI: in WSL der Linux-Pfad (E100). */
|
|
319
|
+
cliPfad(pfad) {
|
|
320
|
+
return cliPfad(pfad);
|
|
321
|
+
}
|
|
322
|
+
/** Wo die geteilte `.mcp.json` eines Arbeitsverzeichnisses liegt, in WSL als Linux-Pfad (E100). */
|
|
323
|
+
mcpJsonPfad(pfad) {
|
|
324
|
+
const wsl = wslUncZerlegen(pfad);
|
|
325
|
+
return wsl === undefined ? path.join(pfad, '.mcp.json') : path.posix.join(wsl.linuxPfad, '.mcp.json');
|
|
326
|
+
}
|
|
327
|
+
/** Die geteilte `.mcp.json` eines Arbeitsverzeichnisses, in WSL über die Bridge gelesen (E100). */
|
|
328
|
+
async projektMcpJson(pfad) {
|
|
329
|
+
const wsl = wslUncZerlegen(pfad);
|
|
330
|
+
if (wsl === undefined)
|
|
331
|
+
return await projektMcpJsonLesen(pfad);
|
|
332
|
+
const text = await this.harness.bruecke(wsl.distro).dateiLesen(this.mcpJsonPfad(pfad)).catch(() => undefined);
|
|
333
|
+
return text === undefined ? {} : mcpServersAusText(text);
|
|
334
|
+
}
|
|
253
335
|
anGitWurzelGebunden = true;
|
|
254
336
|
ebenen() {
|
|
255
337
|
return ['home', 'arbeitsverzeichnis'];
|
|
@@ -257,12 +339,12 @@ class ClaudeMcp {
|
|
|
257
339
|
hinweisNachAenderung() {
|
|
258
340
|
return 'Claude Code übernimmt die Änderung mit der nächsten Sitzung.';
|
|
259
341
|
}
|
|
342
|
+
/** Scope und cwd eines Ortes; das cwd steht in der Welt der zuständigen CLI (E100). */
|
|
260
343
|
scope(ort) {
|
|
261
|
-
ohneCliZugriffPruefen(this.harness.name, ort, 'MCP-Server');
|
|
262
344
|
if (ort.art === 'home')
|
|
263
345
|
return { scope: 'user' };
|
|
264
346
|
if (ort.art === 'arbeitsverzeichnis')
|
|
265
|
-
return { scope: 'local', cwd: ort.pfad };
|
|
347
|
+
return { scope: 'local', cwd: this.cliPfad(ort.pfad) };
|
|
266
348
|
throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
|
|
267
349
|
}
|
|
268
350
|
/**
|
|
@@ -273,8 +355,9 @@ class ClaudeMcp {
|
|
|
273
355
|
*/
|
|
274
356
|
async einhaengen(name, konfig, ort) {
|
|
275
357
|
const { scope, cwd } = this.scope(ort);
|
|
358
|
+
const cli = await this.cli(ort);
|
|
276
359
|
const json = claudeMcpJson(konfig);
|
|
277
|
-
const { stdout, code } = await
|
|
360
|
+
const { stdout, code } = await cli(['mcp', 'add-json', name, json, '--scope', scope], cwd);
|
|
278
361
|
if (code === 0)
|
|
279
362
|
return;
|
|
280
363
|
const bestehend = await this.eintrag(name, ort);
|
|
@@ -294,15 +377,16 @@ class ClaudeMcp {
|
|
|
294
377
|
*/
|
|
295
378
|
async aushaengen(name, ort) {
|
|
296
379
|
const { scope, cwd } = this.scope(ort);
|
|
297
|
-
const
|
|
380
|
+
const cli = await this.cli(ort);
|
|
381
|
+
const { stdout, code } = await cli(['mcp', 'remove', name, '--scope', scope], cwd);
|
|
298
382
|
if (code === 0)
|
|
299
383
|
return;
|
|
300
384
|
if ((await this.eintrag(name, ort)) !== undefined)
|
|
301
385
|
throw new Error(`claude mcp remove ${name} scheiterte (Code ${code}): ${stdout.trim()}`);
|
|
302
386
|
if (ort.art === 'arbeitsverzeichnis') {
|
|
303
|
-
const geteilt = await
|
|
387
|
+
const geteilt = await this.projektMcpJson(ort.pfad);
|
|
304
388
|
if (name in geteilt)
|
|
305
|
-
throw new McpNurGeteilterFundort({ harness: this.harness.name, ort }, name,
|
|
389
|
+
throw new McpNurGeteilterFundort({ harness: this.harness.name, ort }, name, this.mcpJsonPfad(ort.pfad));
|
|
306
390
|
}
|
|
307
391
|
}
|
|
308
392
|
async eingehaengte(ort) {
|
|
@@ -310,16 +394,14 @@ class ClaudeMcp {
|
|
|
310
394
|
}
|
|
311
395
|
/**
|
|
312
396
|
* Die geteilte `.mcp.json` eines Arbeitsverzeichnisses (E61-Nachtrag): im Alltag nur gelesen,
|
|
313
|
-
* weder beim Ein- noch beim Aushängen angefasst. Home kennt keine solche Datei.
|
|
397
|
+
* weder beim Ein- noch beim Aushängen angefasst. Home kennt keine solche Datei. In WSL liegt sie
|
|
398
|
+
* im Linux-Dateisystem und wird über die Bridge gelesen, der Pfad steht als Linux-Pfad da (E100).
|
|
314
399
|
*/
|
|
315
400
|
async geteilteFundorte(ort) {
|
|
316
401
|
if (ort.art !== 'arbeitsverzeichnis')
|
|
317
402
|
return [];
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return [];
|
|
321
|
-
const datei = path.join(ort.pfad, '.mcp.json');
|
|
322
|
-
const server = await projektMcpJsonLesen(ort.pfad);
|
|
403
|
+
const datei = this.mcpJsonPfad(ort.pfad);
|
|
404
|
+
const server = await this.projektMcpJson(ort.pfad);
|
|
323
405
|
return Object.keys(server).map((name) => ({ name, datei, scope: 'project' }));
|
|
324
406
|
}
|
|
325
407
|
/**
|
|
@@ -341,12 +423,13 @@ class ClaudeMcp {
|
|
|
341
423
|
const ziel = { harness: this.harness.name, ort };
|
|
342
424
|
if (ort.art !== 'arbeitsverzeichnis')
|
|
343
425
|
throw new McpUebernahmeNichtMoeglich(ziel);
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
const block = (await projektMcpJsonLesen(ort.pfad))[name];
|
|
426
|
+
const datei = this.mcpJsonPfad(ort.pfad);
|
|
427
|
+
const block = (await this.projektMcpJson(ort.pfad))[name];
|
|
347
428
|
if (block === undefined)
|
|
348
429
|
throw new McpNichtInGeteilterDatei(ziel, name, datei);
|
|
349
|
-
const
|
|
430
|
+
const cli = await this.cli(ort);
|
|
431
|
+
const cwd = this.cliPfad(ort.pfad);
|
|
432
|
+
const hin = await cli(['mcp', 'add-json', name, JSON.stringify(block), '--scope', 'local'], cwd);
|
|
350
433
|
if (hin.code !== 0) {
|
|
351
434
|
// Der einzige harmlose Fehlschlag: derselbe Block liegt schon im privaten Scope, etwa weil ein
|
|
352
435
|
// früherer Lauf zwischen den beiden Schritten abgebrochen ist. Ein abweichender Block wird
|
|
@@ -357,10 +440,10 @@ class ClaudeMcp {
|
|
|
357
440
|
if (!tiefGleich(bestehend, block))
|
|
358
441
|
throw new McpAbweichenderEintrag(ziel, name);
|
|
359
442
|
}
|
|
360
|
-
const weg = await
|
|
443
|
+
const weg = await cli(['mcp', 'remove', name, '--scope', 'project'], cwd);
|
|
361
444
|
// Nicht der Code entscheidet, sondern die Datei danach: steht der Name nicht mehr drin, ist die
|
|
362
445
|
// Übernahme fertig, egal wie die CLI den Fall genannt hat.
|
|
363
|
-
if (weg.code !== 0 && name in (await
|
|
446
|
+
if (weg.code !== 0 && name in (await this.projektMcpJson(ort.pfad)))
|
|
364
447
|
throw new McpUebernahmeUnvollstaendig(ziel, name, datei, weg.stdout.trim());
|
|
365
448
|
return { datei };
|
|
366
449
|
}
|
|
@@ -375,23 +458,35 @@ class ClaudeMcp {
|
|
|
375
458
|
* Ordner in einem Git-Repo, ist der Schlüssel die Git-Wurzel, nicht der Ordner (E91, belegt an
|
|
376
459
|
* 2.1.285): für einen Unterordner liest diese Methode deshalb nichts, und der Kern sperrt dort
|
|
377
460
|
* die MCP-Operationen, bevor sie den Adapter erreichen.
|
|
461
|
+
*
|
|
462
|
+
* In WSL (E100) ist es die `~/.claude.json` der Distro, über die Bridge gelesen; der Schlüssel ist
|
|
463
|
+
* der Linux-Pfad, zusätzlich in der Distro aufgelöst (`realpath`).
|
|
378
464
|
*/
|
|
379
465
|
async mcpServersAm(ort) {
|
|
380
|
-
|
|
381
|
-
const daten = await this.claudeJsonLesen();
|
|
466
|
+
const distro = wslDistroVonOrt(ort);
|
|
467
|
+
const daten = await this.claudeJsonLesen(distro);
|
|
382
468
|
if (ort.art === 'home')
|
|
383
469
|
return mcpServersVon(daten);
|
|
384
470
|
if (ort.art === 'arbeitsverzeichnis') {
|
|
385
471
|
const projects = objektFeld(daten, 'projects');
|
|
386
|
-
const
|
|
387
|
-
|
|
472
|
+
const pfad = this.cliPfad(ort.pfad);
|
|
473
|
+
const direkt = objektFeld(projects, pfad);
|
|
474
|
+
if (direkt !== undefined)
|
|
475
|
+
return mcpServersVon(direkt);
|
|
476
|
+
const echt = distro === undefined
|
|
477
|
+
? aufgeloest(pfad)
|
|
478
|
+
: await this.harness.bruecke(distro).aufgeloest(pfad).catch(() => undefined);
|
|
479
|
+
return mcpServersVon(echt === undefined ? undefined : objektFeld(projects, echt));
|
|
388
480
|
}
|
|
389
481
|
throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
|
|
390
482
|
}
|
|
391
|
-
|
|
483
|
+
/** `~/.claude.json`: unter Windows die im primären Homedir, in WSL die der Distro (E100). */
|
|
484
|
+
async claudeJsonLesen(distro) {
|
|
392
485
|
try {
|
|
393
|
-
const text =
|
|
394
|
-
|
|
486
|
+
const text = distro === undefined
|
|
487
|
+
? await readFile(path.join(this.home(), '.claude.json'), 'utf8')
|
|
488
|
+
: await this.harness.bruecke(distro).claudeJsonLesen();
|
|
489
|
+
return text === undefined ? undefined : JSON.parse(ohneBom(text));
|
|
395
490
|
}
|
|
396
491
|
catch {
|
|
397
492
|
return undefined;
|
|
@@ -404,42 +499,123 @@ export class ClaudeCode {
|
|
|
404
499
|
ausfuehrer;
|
|
405
500
|
/** Zugriff je Ort (E78): lokal wie bisher, für ein WSL-Heim die Bridge. */
|
|
406
501
|
zugriffFuer;
|
|
407
|
-
/**
|
|
502
|
+
/** Die WSL-Anbindung (E100): claude-CLI je Ort, Bridge je Distro, Distros der eingetragenen Orte. */
|
|
503
|
+
wsl;
|
|
504
|
+
/**
|
|
505
|
+
* Plugins (E16) und MCP-Server (E61) setzt `vorhanden()`, sobald irgendwo eine claude-CLI
|
|
506
|
+
* antwortet (E100, E101): unter Windows oder in der Distro eines eingetragenen WSL-Orts. Ob es an
|
|
507
|
+
* einem bestimmten Ort geht, entscheidet der Ort (`cliPruefen`).
|
|
508
|
+
*/
|
|
408
509
|
plugins;
|
|
409
|
-
/** Wird von `vorhanden()` gesetzt, sobald die claude-CLI auf dem PATH liegt (E61). */
|
|
410
510
|
mcp;
|
|
411
|
-
|
|
511
|
+
/** Ob die claude-CLI antwortet, je Welt einmal je Lauf: Schlüssel '' für Windows, sonst die Distro. */
|
|
512
|
+
erreichbarkeit = new Map();
|
|
412
513
|
/**
|
|
413
514
|
* `heime` sagt, wo die Homedirs liegen (E78): das primäre und, seit F87, zusätzliche in einer
|
|
414
515
|
* WSL-Distro. Ein blanker Pfad steht weiterhin für „nur dieses eine Home", damit Tests und
|
|
415
|
-
* Aufrufer ohne Zustand unverändert bleiben.
|
|
516
|
+
* Aufrufer ohne Zustand unverändert bleiben. `wsl` verdrahtet `bauen` (E100); ohne Angabe gehen
|
|
517
|
+
* die Aufrufe an das echte wsl.exe, und als WSL-Orte zählen nur die zusätzlichen Heime.
|
|
416
518
|
*/
|
|
417
|
-
constructor(heime, ausfuehrer = claudeAusfuehrer, zugriffFuer) {
|
|
519
|
+
constructor(heime, ausfuehrer = claudeAusfuehrer, zugriffFuer, wsl) {
|
|
418
520
|
this.heime = typeof heime === 'string' ? nurHome(heime) : heime;
|
|
419
521
|
this.ausfuehrer = ausfuehrer;
|
|
420
522
|
const lokal = new LokalerZugriff();
|
|
421
523
|
this.zugriffFuer = zugriffFuer ?? (() => lokal);
|
|
524
|
+
const bruecken = new Map();
|
|
525
|
+
const langeBridge = wslAusfuehrerMit(120_000);
|
|
526
|
+
this.wsl = {
|
|
527
|
+
ausfuehrerFuer: wsl?.ausfuehrerFuer ??
|
|
528
|
+
((ort) => {
|
|
529
|
+
const distro = wslDistroVonOrt(ort);
|
|
530
|
+
return distro === undefined ? this.ausfuehrer : wslClaudeAusfuehrer(distro, langeBridge);
|
|
531
|
+
}),
|
|
532
|
+
bruecke: wsl?.bruecke ??
|
|
533
|
+
((distro) => {
|
|
534
|
+
let b = bruecken.get(distro);
|
|
535
|
+
if (b === undefined)
|
|
536
|
+
bruecken.set(distro, (b = new WslBruecke(distro)));
|
|
537
|
+
return b;
|
|
538
|
+
}),
|
|
539
|
+
distros: wsl?.distros ??
|
|
540
|
+
(async () => this.heime
|
|
541
|
+
.zusaetzliche(this.name)
|
|
542
|
+
.map((heim) => wslDistroVonOrt({ art: 'home', heim: heim.adresse }))
|
|
543
|
+
.filter((distro) => distro !== undefined)),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** Die claude-CLI, die für diesen Ort zuständig ist (E100): die des Rechners oder die der Distro. */
|
|
547
|
+
ausfuehrerFuer(ort) {
|
|
548
|
+
return this.wsl.ausfuehrerFuer(ort);
|
|
549
|
+
}
|
|
550
|
+
/** Die Bridge in eine Distro, für das Lesen ihrer Dateien (E100). */
|
|
551
|
+
bruecke(distro) {
|
|
552
|
+
return this.wsl.bruecke(distro);
|
|
553
|
+
}
|
|
554
|
+
/** Ob an diesem Ort eine claude-CLI antwortet (`claude --version`), je Welt einmal je Lauf. */
|
|
555
|
+
async erreichbar(ort) {
|
|
556
|
+
const schluessel = wslDistroVonOrt(ort) ?? '';
|
|
557
|
+
let versprechen = this.erreichbarkeit.get(schluessel);
|
|
558
|
+
if (versprechen === undefined) {
|
|
559
|
+
const cli = schluessel === '' ? this.ausfuehrer : this.ausfuehrerFuer(ort);
|
|
560
|
+
versprechen = cli(['--version'])
|
|
561
|
+
.then(({ code }) => code === 0)
|
|
562
|
+
.catch(() => false);
|
|
563
|
+
this.erreichbarkeit.set(schluessel, versprechen);
|
|
564
|
+
}
|
|
565
|
+
return await versprechen;
|
|
566
|
+
}
|
|
567
|
+
/** Wirft `ClaudeCliNichtErreichbar`, wenn an diesem Ort keine claude-CLI antwortet (E100). */
|
|
568
|
+
async cliPruefen(ort) {
|
|
569
|
+
if (!(await this.erreichbar(ort)))
|
|
570
|
+
throw new ClaudeCliNichtErreichbar({ harness: this.name, ort }, wslDistroVonOrt(ort));
|
|
422
571
|
}
|
|
423
572
|
/**
|
|
424
573
|
* Claude Code liegt auf dem Rechner, wenn es sein Home angelegt hat — oder wenn ein zusätzliches
|
|
425
574
|
* Homedir eingetragen ist (E78): wer eines eingetragen hat, hat Claude Code dort laufen, und ein
|
|
426
|
-
* Windows-Home ohne `.claude` soll die WSL-Ziele nicht mit verschwinden lassen.
|
|
427
|
-
*
|
|
575
|
+
* Windows-Home ohne `.claude` soll die WSL-Ziele nicht mit verschwinden lassen.
|
|
576
|
+
*
|
|
577
|
+
* Die Plugin- und die MCP-Fähigkeit (E100, E101) kommen dazu, sobald `claude --version` unter
|
|
578
|
+
* Windows durchläuft — oder, wenn dort keine CLI liegt, sobald sie in der Distro eines
|
|
579
|
+
* eingetragenen WSL-Orts antwortet. Die Distros werden erst dann befragt, nacheinander bis zur
|
|
580
|
+
* ersten Antwort, damit ein Rechner mit Windows-claude keinen wsl.exe-Start zahlt. Jede Welt wird
|
|
581
|
+
* einmal je Lauf geprüft; in der GUI kommt ein später eingetragener WSL-Ort beim nächsten Aufruf
|
|
582
|
+
* dazu, ohne die schon geprüften erneut zu fragen.
|
|
428
583
|
*/
|
|
429
584
|
async vorhanden() {
|
|
430
585
|
const primaer = this.heime.primaer(this.name);
|
|
431
586
|
const da = (await stat(path.join(primaer, CLAUDE_VERZEICHNIS)).catch(() => undefined)) !== undefined ||
|
|
432
587
|
this.heime.zusaetzliche(this.name).length > 0;
|
|
433
|
-
if (
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
if (code === 0) {
|
|
437
|
-
this.plugins = new ClaudePlugins(this, this.ausfuehrer);
|
|
438
|
-
this.mcp = new ClaudeMcp(this, this.ausfuehrer, () => this.heime.primaer(this.name));
|
|
439
|
-
}
|
|
588
|
+
if (this.mcp === undefined && (await this.irgendwoErreichbar())) {
|
|
589
|
+
this.plugins = new ClaudePlugins(this);
|
|
590
|
+
this.mcp = new ClaudeMcp(this, () => this.heime.primaer(this.name));
|
|
440
591
|
}
|
|
441
592
|
return da;
|
|
442
593
|
}
|
|
594
|
+
/** Antwortet irgendwo eine claude-CLI: unter Windows oder in einer Distro der WSL-Orte (E100). */
|
|
595
|
+
async irgendwoErreichbar() {
|
|
596
|
+
if (await this.erreichbar({ art: 'home' }))
|
|
597
|
+
return true;
|
|
598
|
+
const distros = await this.wsl.distros().catch(() => []);
|
|
599
|
+
for (const distro of new Set(distros))
|
|
600
|
+
if (await this.erreichbar(distroOrt(distro)))
|
|
601
|
+
return true;
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Je Welt mit antwortender claude-CLI ein Ort, der für sie steht (E101): das Windows-Home und je
|
|
606
|
+
* Distro der eingetragenen WSL-Orte ihr Heim. Anders als `irgendwoErreichbar` fragt es alle
|
|
607
|
+
* Distros, auch wenn Windows schon antwortet — jede Welt hat ihre eigenen Marketplaces.
|
|
608
|
+
*/
|
|
609
|
+
async erreichbareWelten() {
|
|
610
|
+
const welten = [];
|
|
611
|
+
if (await this.erreichbar({ art: 'home' }))
|
|
612
|
+
welten.push({ art: 'home' });
|
|
613
|
+
const distros = await this.wsl.distros().catch(() => []);
|
|
614
|
+
for (const distro of new Set(distros))
|
|
615
|
+
if (await this.erreichbar(distroOrt(distro)))
|
|
616
|
+
welten.push(distroOrt(distro));
|
|
617
|
+
return welten;
|
|
618
|
+
}
|
|
443
619
|
orte() {
|
|
444
620
|
return ['home', 'arbeitsverzeichnis'];
|
|
445
621
|
}
|
package/dist/adapter/git.js
CHANGED
|
@@ -121,10 +121,39 @@ async function dateienAufPlatte(wurzel, unter = '') {
|
|
|
121
121
|
}
|
|
122
122
|
return dateien;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Wartezeiten zwischen den Versuchen bei belegter git-Sperre (E99): 5 Versuche, zusammen rund 3 s.
|
|
126
|
+
* Tests übergeben kürzere über den Konstruktor.
|
|
127
|
+
*/
|
|
128
|
+
export const SPERRE_WARTEZEITEN_MS = [200, 400, 800, 1600];
|
|
129
|
+
/** Meldung, mit der git eine belegte Sperrdatei anzeigt (E99). */
|
|
130
|
+
const SPERRE_BELEGT = /Unable to create '[^']+\.lock': File exists/;
|
|
124
131
|
export class GitQuellen {
|
|
125
132
|
wurzel;
|
|
126
|
-
|
|
133
|
+
sperreWartezeitenMs;
|
|
134
|
+
constructor(wurzel, optionen = {}) {
|
|
127
135
|
this.wurzel = wurzel;
|
|
136
|
+
this.sperreWartezeitenMs = optionen.sperreWartezeitenMs ?? SPERRE_WARTEZEITEN_MS;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Wiederholt einen git-Aufruf, wenn eine fremde Sperre ihn blockiert (E99): GUI-Aktionen und der
|
|
140
|
+
* Start-Lauf rufen git parallel auf, und git bricht bei `….lock: File exists` sofort ab. Nur
|
|
141
|
+
* diese Meldung wird wiederholt, jeder andere Fehler gilt sofort. Nach dem letzten Versuch bleibt
|
|
142
|
+
* die bisherige Fehlermeldung. hmgr löscht fremde Sperren nie, auch keine verwaisten.
|
|
143
|
+
*/
|
|
144
|
+
async mitSperrWiederholung(aufruf) {
|
|
145
|
+
for (let versuch = 0;; versuch++) {
|
|
146
|
+
try {
|
|
147
|
+
return await aufruf();
|
|
148
|
+
}
|
|
149
|
+
catch (fehler) {
|
|
150
|
+
const wartezeit = this.sperreWartezeitenMs[versuch];
|
|
151
|
+
const meldung = fehler instanceof Error ? fehler.message : String(fehler);
|
|
152
|
+
if (wartezeit === undefined || !SPERRE_BELEGT.test(meldung))
|
|
153
|
+
throw fehler;
|
|
154
|
+
await new Promise((fertig) => setTimeout(fertig, wartezeit));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
128
157
|
}
|
|
129
158
|
/**
|
|
130
159
|
* stdin ist zu und git darf nicht nach Zugangsdaten fragen, sonst hängt ein wartender Aufruf
|
|
@@ -132,6 +161,9 @@ export class GitQuellen {
|
|
|
132
161
|
* als dort, ein erster Clone über ein langsames Netz braucht Zeit.
|
|
133
162
|
*/
|
|
134
163
|
async git(args, cwd) {
|
|
164
|
+
return await this.mitSperrWiederholung(() => this.gitEinmal(args, cwd));
|
|
165
|
+
}
|
|
166
|
+
async gitEinmal(args, cwd) {
|
|
135
167
|
return await new Promise((erfuellen, ablehnen) => {
|
|
136
168
|
const kind = spawn('git', args, {
|
|
137
169
|
cwd,
|
|
@@ -155,6 +187,9 @@ export class GitQuellen {
|
|
|
155
187
|
}
|
|
156
188
|
/** Wie `git`, aber mit Text auf stdin und Bytes als Ergebnis: `execFile` kennt keine Eingabe. */
|
|
157
189
|
async gitMitEingabe(args, cwd, eingabe) {
|
|
190
|
+
return await this.mitSperrWiederholung(() => this.gitMitEingabeEinmal(args, cwd, eingabe));
|
|
191
|
+
}
|
|
192
|
+
async gitMitEingabeEinmal(args, cwd, eingabe) {
|
|
158
193
|
return await new Promise((erfuellen, ablehnen) => {
|
|
159
194
|
const kind = spawn('git', args, { cwd, timeout: 300_000, killSignal: 'SIGKILL' });
|
|
160
195
|
const stuecke = [];
|
package/dist/adapter/wsl.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
110
|
-
|
|
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
|
|
234
|
-
|
|
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
|
|
629
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
1210
|
-
return
|
|
1211
|
-
|
|
1212
|
-
|
|
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.
|
|
145
|
+
<p>Dein Katalog aus <span class="mono" id="mcpkatalogpfad">~/.hmgr/mcp.yaml</span>. Häng einen Server dorthin ein, wo du ihn brauchst: in ein Arbeitsverzeichnis, damit er nur dort Kontext kostet, oder global. Den Katalog selbst pflegst du in der Datei, hmgr liest sie nur. In WSL – im Homedir einer Distro und in Arbeitsverzeichnissen unter \\wsl$\… – hängt hmgr den Server über die claude-CLI der Distro ein. Plugins gehen dort auf demselben Weg.</p>
|
|
146
146
|
</div>
|
|
147
147
|
|
|
148
148
|
<div class="mcpliste" id="mcpliste"></div>
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
<section id="ansicht-plugins" hidden>
|
|
168
168
|
<div class="ansichtskopf">
|
|
169
169
|
<h1>Plugins</h1>
|
|
170
|
-
<p>Ein Marketplace ist eine Quelle mit einer <span class="mono">marketplace.json</span>. Die Plugins darin bündeln Skills und können voneinander abhängen. Harnesses ohne Plugin-Konzept bekommen von hmgr stattdessen die flache Skill-Liste verlinkt.</p>
|
|
170
|
+
<p>Ein Marketplace ist eine Quelle mit einer <span class="mono">marketplace.json</span>. Die Plugins darin bündeln Skills und können voneinander abhängen. Harnesses ohne Plugin-Konzept bekommen von hmgr stattdessen die flache Skill-Liste verlinkt. In WSL installiert hmgr Plugins über die claude-CLI der Distro; jede Claude-Installation führt ihre eigene Marketplace-Liste, hmgr trägt den Marketplace dort ein und zieht ihn beim Aktualisieren nach.</p>
|
|
171
171
|
</div>
|
|
172
172
|
<div id="pluginliste"></div>
|
|
173
173
|
</section>
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
3
|
+
import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncBilden, wslUncZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
4
4
|
import { mitBegrenzung } from "./begrenzung.js";
|
|
5
5
|
import { passenderRef } from "./version.js";
|
|
6
6
|
import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, kannZusaetzlicheHeime, } from "../ports/harness.js";
|
|
7
7
|
/** So viele `plugin list`-Prozesse laufen im Ist-Zustand höchstens gleichzeitig (E93). */
|
|
8
8
|
const PLUGIN_ABFRAGEN_GLEICHZEITIG = 6;
|
|
9
|
+
/**
|
|
10
|
+
* Die Welt eines Orts als Text für Bericht und Verlauf (E101), in der Schreibweise von `--ort`:
|
|
11
|
+
* „home" für die Installation dieses Rechners, „wsl:<distro>" für die einer Distro.
|
|
12
|
+
*/
|
|
13
|
+
export function pluginWeltText(ort) {
|
|
14
|
+
const distro = wslDistroVonOrt(ort);
|
|
15
|
+
return distro === undefined ? 'home' : `wsl:${distro}`;
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
18
|
* Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
|
|
11
19
|
* Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
|
|
@@ -357,6 +365,15 @@ export class Anwendungsfaelle {
|
|
|
357
365
|
async mcpGitWurzelFuer(faehig, ziel) {
|
|
358
366
|
if (faehig.anGitWurzelGebunden !== true || ziel.ort.art !== 'arbeitsverzeichnis')
|
|
359
367
|
return null;
|
|
368
|
+
// In WSL fragt hmgr git in der Distro (E100); die Wurzel kommt in der Schreibweise des
|
|
369
|
+
// gespeicherten Pfads zurück, damit die Meldung einen Pfad nennt, den `hmgr dir add` nimmt.
|
|
370
|
+
const wsl = wslUncZerlegen(ziel.ort.pfad);
|
|
371
|
+
if (wsl !== undefined) {
|
|
372
|
+
const funde = await this.p.wsl.gitWurzel(wsl.distro, wsl.linuxPfad).catch(() => null);
|
|
373
|
+
if (funde === null || funde.relativerPfad === '')
|
|
374
|
+
return null;
|
|
375
|
+
return wslUncBilden(ziel.ort.pfad, funde.wurzel) ?? funde.wurzel;
|
|
376
|
+
}
|
|
360
377
|
const kopie = await this.p.quellen.arbeitskopie(ziel.ort.pfad).catch(() => null);
|
|
361
378
|
if (kopie === null || kopie.relativerPfad === '')
|
|
362
379
|
return null;
|
|
@@ -464,18 +481,17 @@ export class Anwendungsfaelle {
|
|
|
464
481
|
if (!kannMcp(harness))
|
|
465
482
|
continue;
|
|
466
483
|
const ebenen = harness.mcp.ebenen();
|
|
467
|
-
//
|
|
468
|
-
//
|
|
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(
|
|
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
|
-
|
|
478
|
-
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
494
|
+
ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
|
|
479
495
|
}
|
|
480
496
|
return ziele;
|
|
481
497
|
}
|
|
@@ -1526,8 +1542,9 @@ export class Anwendungsfaelle {
|
|
|
1526
1542
|
if (!harness.orte().includes(ziel.ort.art))
|
|
1527
1543
|
throw new OrtNichtUnterstuetzt(harness.name, ziel.ort.art);
|
|
1528
1544
|
if (kannPlugins(harness)) {
|
|
1529
|
-
|
|
1530
|
-
|
|
1545
|
+
// Die Marketplace-Liste gehört zur Welt des Ziels (E101): ein WSL-Ziel trägt ihn in der Distro ein.
|
|
1546
|
+
if (!(await harness.plugins.marketplaceBekannt(fund.plugin.marketplace, ziel.ort)))
|
|
1547
|
+
await harness.plugins.marketplaceHinzufuegen(fund.plugin.marketplace, marketplaceUrl(fund.quelle), ziel.ort);
|
|
1531
1548
|
await harness.vorbereiten(ziel.ort);
|
|
1532
1549
|
await harness.plugins.installieren(fund.plugin.name, fund.plugin.marketplace, ziel);
|
|
1533
1550
|
return { plugin: fund.plugin, art: 'installiert', skills: [], imZeiger: false };
|
|
@@ -1860,13 +1877,26 @@ export class Anwendungsfaelle {
|
|
|
1860
1877
|
}
|
|
1861
1878
|
this.nachgezogen.add(quelleSchluessel(quelle));
|
|
1862
1879
|
}
|
|
1863
|
-
// Nach den Checkouts die Plugins nachziehen (E14)
|
|
1880
|
+
// Nach den Checkouts die Plugins nachziehen (E14), je Welt mit erreichbarer Installation
|
|
1881
|
+
// (E101): Windows und jede Distro der eingetragenen WSL-Orte haben ihre eigenen Marketplaces.
|
|
1882
|
+
// Ein Fehlschlag in einer Welt steht im Ergebnis und bricht den Lauf nicht ab.
|
|
1864
1883
|
const pluginHarnesses = [];
|
|
1884
|
+
const pluginFehler = [];
|
|
1865
1885
|
for (const harness of nurSchluessel !== undefined ? [] : await this.vorhandeneHarnesses()) {
|
|
1866
1886
|
if (!kannPlugins(harness))
|
|
1867
1887
|
continue;
|
|
1868
|
-
|
|
1869
|
-
|
|
1888
|
+
let erfolgreich = false;
|
|
1889
|
+
for (const ort of await harness.plugins.welten()) {
|
|
1890
|
+
try {
|
|
1891
|
+
await harness.plugins.aktualisieren(ort);
|
|
1892
|
+
erfolgreich = true;
|
|
1893
|
+
}
|
|
1894
|
+
catch (fehler) {
|
|
1895
|
+
pluginFehler.push({ ziel: { harness: harness.name, ort }, fehler: fehlertext(fehler) });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (erfolgreich)
|
|
1899
|
+
pluginHarnesses.push(harness.name);
|
|
1870
1900
|
}
|
|
1871
1901
|
// Bundles vor dem Aufräumen (E69, F64=O1): der Abgleich legt und entfernt Links, und erst
|
|
1872
1902
|
// danach steht fest, in welchen Checkout noch ein Link zeigt. Ein Fehlschlag je Bundle steckt
|
|
@@ -1878,15 +1908,16 @@ export class Anwendungsfaelle {
|
|
|
1878
1908
|
const doctorErgebnis = nurSchluessel !== undefined
|
|
1879
1909
|
? { fehlendeArbeitsverzeichnisse: [], vergessen: [] }
|
|
1880
1910
|
: await this.doctor({ rueckfragen });
|
|
1881
|
-
await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis));
|
|
1882
|
-
return { checkouts, pluginHarnesses, bundles, aufraeumen, doctor: doctorErgebnis };
|
|
1911
|
+
await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis));
|
|
1912
|
+
return { checkouts, pluginHarnesses, pluginFehler, bundles, aufraeumen, doctor: doctorErgebnis };
|
|
1883
1913
|
}
|
|
1884
1914
|
/**
|
|
1885
|
-
* Ereignisse eines Aktualisieren-Laufs für den Verlauf (E80):
|
|
1886
|
-
*
|
|
1887
|
-
* Auskunft) — die bisherige Zeile „Plugins aktualisiert" war deshalb geschönt.
|
|
1915
|
+
* Ereignisse eines Aktualisieren-Laufs für den Verlauf (E80): eine Plugin-Änderung bleibt
|
|
1916
|
+
* draußen, weil sie sich nicht messen lässt (`claude plugin marketplace update` liefert keine
|
|
1917
|
+
* Auskunft) — die bisherige Zeile „Plugins aktualisiert" war deshalb geschönt. Ein Fehlschlag
|
|
1918
|
+
* je Welt (E101) steht dagegen drin.
|
|
1888
1919
|
*/
|
|
1889
|
-
aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis) {
|
|
1920
|
+
aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis) {
|
|
1890
1921
|
const ereignisse = [];
|
|
1891
1922
|
for (const { checkout, aktualisierung, geaenderteSkills } of checkouts) {
|
|
1892
1923
|
if (aktualisierung.ergebnis === 'aktualisiert')
|
|
@@ -1905,6 +1936,8 @@ export class Anwendungsfaelle {
|
|
|
1905
1936
|
fehler: aktualisierung.fehler,
|
|
1906
1937
|
});
|
|
1907
1938
|
}
|
|
1939
|
+
for (const { ziel, fehler } of pluginFehler)
|
|
1940
|
+
ereignisse.push({ art: 'fehler', betrifft: `Plugins ${ziel.harness} ${pluginWeltText(ziel.ort)}`, fehler });
|
|
1908
1941
|
for (const eintrag of bundles) {
|
|
1909
1942
|
if (eintrag.ergebnis === 'fehlgeschlagen')
|
|
1910
1943
|
ereignisse.push({ art: 'fehler', betrifft: eintrag.bundle, fehler: eintrag.fehler ?? '' });
|
|
@@ -2365,20 +2398,18 @@ export class Anwendungsfaelle {
|
|
|
2365
2398
|
}
|
|
2366
2399
|
// Installierte Plugins je Ziel vorab, mit begrenzter Parallelität (E93): `claude plugin list`
|
|
2367
2400
|
// ist ein Prozess je Ziel (cwd-gebunden, kein Sammelaufruf), nacheinander dauert das bei vielen
|
|
2368
|
-
// Arbeitsverzeichnissen Sekunden.
|
|
2369
|
-
//
|
|
2370
|
-
// WSL-Distro nicht, ihre user-Liste gehörte zum Windows-Home.
|
|
2401
|
+
// Arbeitsverzeichnissen Sekunden. WSL-Ziele fragen die claude-CLI ihrer Distro (E101); ein Ort
|
|
2402
|
+
// ohne erreichbare CLI ergibt eine leere Liste.
|
|
2371
2403
|
const pluginListen = new Map();
|
|
2372
2404
|
if (mitPlugins) {
|
|
2373
2405
|
const abfragen = [];
|
|
2374
2406
|
for (const { harness, ziele } of plan)
|
|
2375
2407
|
if (kannPlugins(harness))
|
|
2376
2408
|
for (const ziel of ziele)
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
});
|
|
2409
|
+
abfragen.push({
|
|
2410
|
+
schluessel: zielSchluessel(ziel),
|
|
2411
|
+
abruf: () => harness.plugins.installierte(ziel).catch(() => []),
|
|
2412
|
+
});
|
|
2382
2413
|
const listen = await mitBegrenzung(PLUGIN_ABFRAGEN_GLEICHZEITIG, abfragen.map((a) => a.abruf));
|
|
2383
2414
|
abfragen.forEach((a, i) => pluginListen.set(a.schluessel, listen[i]));
|
|
2384
2415
|
}
|
package/dist/kern/modell.js
CHANGED
|
@@ -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
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
constructor(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
/**
|