hmgr 0.0.5 → 0.0.7

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,5 +1,12 @@
1
1
  // Begriffe aus CONTEXT.md. Nur Werte und Regeln, keine Pfade zu Dateien außerhalb des Modells.
2
2
  import { ortSchluessel, urlOrt } from "./quelle-ort.js";
3
+ /**
4
+ * Identität eines Bundles (E69): Quelle der Definition plus Name. Zwei Quellen dürfen denselben
5
+ * Bundle-Namen führen, mehrdeutig wird es erst beim Suchen, wie bei Skills (E12).
6
+ */
7
+ export function bundleSchluessel(bundle) {
8
+ return `${quelleSchluessel(bundle.quelle)}#${bundle.name}`;
9
+ }
3
10
  /** Ansprache eines Plugins im Zeiger und gegenüber der claude-CLI: `<plugin>@<marketplace>`. */
4
11
  export function pluginSchluessel(plugin) {
5
12
  return `${plugin.name}@${plugin.marketplace}`;
@@ -63,10 +70,15 @@ export function quelleSchluessel(quelle) {
63
70
  const ort = urlOrt(quelle.url);
64
71
  return ort === undefined ? quelle.url : ortSchluessel(ort);
65
72
  }
66
- /** Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche. */
73
+ /**
74
+ * Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche.
75
+ * Trenner sind `/` und `\` (E65/B7-Nachbar B8): unter Windows kommt der Pfad als `C:\work\quelle`
76
+ * herein und ergab sonst den Namen „\work\quelle". Ein POSIX-Ordnername mit Backslash wird dadurch
77
+ * zerteilt — akzeptiert, das hier ist reine Anzeige-Ableitung und geht in keinen Schlüssel ein.
78
+ */
67
79
  function letztesSegment(text) {
68
- const bereinigt = text.replace(/\/+$/, '');
69
- const segmente = bereinigt.split('/').filter((s) => s.length > 0);
80
+ const bereinigt = text.replace(/[/\\]+$/, '');
81
+ const segmente = bereinigt.split(/[/\\]/).filter((s) => s.length > 0);
70
82
  return segmente[segmente.length - 1] ?? bereinigt;
71
83
  }
72
84
  /**
@@ -130,6 +142,25 @@ export class NameBelegt extends Error {
130
142
  this.skillName = skillName;
131
143
  }
132
144
  }
145
+ /**
146
+ * Der Link am Ziel ließ sich im Dateisystem nicht anlegen (E66). Ohne diesen Fehler käme ein roher
147
+ * Node-Stack beim Nutzer an, der weder Pfad noch Ausweg nennt. Der Handlungshinweis steht mit in
148
+ * der Meldung statt in `naechsterSchritt` der CLI: er nennt kein hmgr-Kommando, sondern eine
149
+ * Einstellung des Betriebssystems, und die GUI zeigt nur die Meldung.
150
+ */
151
+ export class LinkAnlegenFehlgeschlagen extends Error {
152
+ linkPfad;
153
+ zielPfad;
154
+ ursache;
155
+ hinweis;
156
+ constructor(linkPfad, zielPfad, ursache, hinweis) {
157
+ super(`Der Link „${linkPfad}" auf „${zielPfad}" ließ sich nicht anlegen: ${ursache}\n${hinweis}`);
158
+ this.linkPfad = linkPfad;
159
+ this.zielPfad = zielPfad;
160
+ this.ursache = ursache;
161
+ this.hinweis = hinweis;
162
+ }
163
+ }
133
164
  export class SkillMehrdeutig extends Error {
134
165
  name;
135
166
  quellen;
@@ -318,3 +349,190 @@ export class SkillOrdnerUngueltig extends Error {
318
349
  this.grund = grund;
319
350
  }
320
351
  }
352
+ /** Ein Name, der im MCP-Katalog nicht steht (E61): mcp add und mcp remove kennen nur Katalog-Einträge. */
353
+ export class McpUnbekannt extends Error {
354
+ mcpName;
355
+ constructor(mcpName) {
356
+ super(`MCP „${mcpName}" steht nicht im Katalog`);
357
+ this.mcpName = mcpName;
358
+ }
359
+ }
360
+ /** Ein Harness-Typ ohne die Fähigkeit MCP-Server (E61): heute nur claude-code und hermes. */
361
+ export class McpFaehigkeitFehlt extends Error {
362
+ harness;
363
+ constructor(harness) {
364
+ super(`Harness ${harness} kennt keine MCP-Server`);
365
+ this.harness = harness;
366
+ }
367
+ }
368
+ /**
369
+ * Ein Ort, an dem dieser Harness-Typ keine MCP-Server verwaltet (E61): die Fähigkeit hängt am
370
+ * Adapter, nicht am Harness-Namen (E16), aber Hermes kennt grundsätzlich keine Projektebene für
371
+ * MCPs, das bekommt eine eigene Erklärung.
372
+ */
373
+ export class McpEbeneNichtUnterstuetzt extends Error {
374
+ harness;
375
+ ort;
376
+ constructor(harness, ort) {
377
+ const erklaerung = harness === 'hermes' && ort === 'arbeitsverzeichnis' ? ' – Hermes kennt keine Projektebene für MCPs' : '';
378
+ super(`${harness} kennt für MCP-Server den Ort „${ort}" nicht${erklaerung}`);
379
+ this.harness = harness;
380
+ this.ort = ort;
381
+ }
382
+ }
383
+ /**
384
+ * Am Ziel liegt unter diesem Namen schon ein MCP-Server, den hmgr nicht selbst gelegt hat oder der
385
+ * vom Katalogblock abweicht (E61). hmgr überschreibt so etwas nicht stillschweigend.
386
+ */
387
+ export class McpAbweichenderEintrag extends Error {
388
+ ziel;
389
+ mcpName;
390
+ constructor(ziel, mcpName) {
391
+ super(`In ${zielSchluessel(ziel)} liegt schon ein MCP-Server „${mcpName}", der nicht von hmgr stammt oder vom ` +
392
+ 'Katalogblock abweicht. hmgr überschreibt ihn nicht stillschweigend.');
393
+ this.ziel = ziel;
394
+ this.mcpName = mcpName;
395
+ }
396
+ }
397
+ /**
398
+ * `aushaengen` trifft einen Namen, der nicht im schreibbaren Bereich liegt, aber in einem geteilten
399
+ * Fundort steht (E61-Nachtrag), etwa der `.mcp.json` einer Projektwurzel bei Claude Code. Anders als
400
+ * der reguläre idempotente Fall (nirgends eingehängt) bleibt hier tatsächlich ein Eintrag liegen:
401
+ * die Meldung darf das nicht als Erfolg verkaufen. Seit E64 gibt es dafür einen Weg in hmgr selbst,
402
+ * die Meldung nennt ihn statt „von Hand ändern".
403
+ */
404
+ export class McpNurGeteilterFundort extends Error {
405
+ ziel;
406
+ mcpName;
407
+ datei;
408
+ constructor(ziel, mcpName, datei) {
409
+ super(`„${mcpName}" steht in der geteilten ${datei}, nicht in deiner privaten Konfiguration. `
410
+ + `hmgr mcp migrate ${mcpName} übernimmt ihn in den privaten Scope und entfernt ihn aus der Datei.`);
411
+ this.ziel = ziel;
412
+ this.mcpName = mcpName;
413
+ this.datei = datei;
414
+ }
415
+ }
416
+ /**
417
+ * Übernehmen (E64) an einem Ort, der keine geteilte Konfigurationsdatei kennt: ein Harness ohne
418
+ * diese Fähigkeit (Hermes kennt keine geteilte Datei) oder eine Ebene ohne sie (bei Claude Code hat
419
+ * nur ein Arbeitsverzeichnis eine `.mcp.json`, das Home nicht).
420
+ */
421
+ export class McpUebernahmeNichtMoeglich extends Error {
422
+ ziel;
423
+ constructor(ziel) {
424
+ super(`In ${zielSchluessel(ziel)} gibt es keine geteilte Konfigurationsdatei, aus der hmgr einen MCP-Server übernehmen könnte.`);
425
+ this.ziel = ziel;
426
+ }
427
+ }
428
+ /** Übernehmen (E64) eines Namens, der in der geteilten Datei gar nicht steht. */
429
+ export class McpNichtInGeteilterDatei extends Error {
430
+ ziel;
431
+ mcpName;
432
+ datei;
433
+ constructor(ziel, mcpName, datei) {
434
+ super(`„${mcpName}" steht nicht in ${datei}, da ist nichts zu übernehmen.`);
435
+ this.ziel = ziel;
436
+ this.mcpName = mcpName;
437
+ this.datei = datei;
438
+ }
439
+ }
440
+ /**
441
+ * Teilerfolg beim Übernehmen (E64): der Eintrag steht schon im privaten Scope, ließ sich aber nicht
442
+ * aus der geteilten Datei entfernen. Die Reihenfolge ist bewusst so gewählt (erst eintragen, dann
443
+ * entfernen), damit in diesem Fall nichts verloren ist. Die Meldung sagt, was der Nutzer jetzt
444
+ * vorfindet, statt nur den Fehlschlag zu nennen.
445
+ */
446
+ export class McpUebernahmeUnvollstaendig extends Error {
447
+ ziel;
448
+ mcpName;
449
+ datei;
450
+ constructor(ziel, mcpName, datei, grund) {
451
+ super(`„${mcpName}" steht jetzt in deiner privaten Konfiguration, ließ sich aber nicht aus ${datei} entfernen: ${grund}. `
452
+ + 'Der Eintrag steht damit doppelt; wirksam ist der private. Entferne ihn aus der Datei von Hand, dann ist die Übernahme fertig.');
453
+ this.ziel = ziel;
454
+ this.mcpName = mcpName;
455
+ this.datei = datei;
456
+ }
457
+ }
458
+ /** Ein Bundle-Name, den keine bekannte Quelle führt (E69). */
459
+ export class BundleUnbekannt extends Error {
460
+ bundleName;
461
+ constructor(bundleName) {
462
+ super(`Bundle „${bundleName}" steht in keiner Quelle`);
463
+ this.bundleName = bundleName;
464
+ }
465
+ }
466
+ /** Denselben Bundle-Namen führen mehrere Quellen (E69, Regel wie E12 bei Skills). */
467
+ export class BundleMehrdeutig extends Error {
468
+ bundleName;
469
+ quellen;
470
+ constructor(bundleName, quellen) {
471
+ super(`Bundle „${bundleName}" liegt in mehreren Quellen: ${quellen.map(quelleSchluessel).join(', ')}`);
472
+ this.bundleName = bundleName;
473
+ this.quellen = quellen;
474
+ }
475
+ }
476
+ /**
477
+ * Die Definition nennt Mitglieder, die sich auf dieser Maschine nicht auflösen lassen (E69, F66b):
478
+ * eine Quelle ohne Zugriff oder ein Pfad, der in ihr kein Skill ist. Ein halbes Bundle wäre stilles
479
+ * Fehlen von Skills, die falsche Fehlersorte (B13) — deshalb wird abgebrochen, bevor irgendetwas
480
+ * verlinkt ist, und bevor eine fehlende Quelle in den Zustand kommt.
481
+ */
482
+ export class BundleMitgliederFehlen extends Error {
483
+ bundleName;
484
+ fehler;
485
+ constructor(bundleName, fehler) {
486
+ const zeilen = fehler.map((f) => ` ${f.adresse} (${f.pfad}): ${f.grund}`).join('\n');
487
+ super(`Bundle „${bundleName}" ist auf dieser Maschine nicht vollständig herstellbar, nichts wurde verlinkt:\n${zeilen}`);
488
+ this.bundleName = bundleName;
489
+ this.fehler = fehler;
490
+ }
491
+ }
492
+ /**
493
+ * Am Ziel liegt unter dem Namen eines Mitglieds schon etwas anderes (E69, F67): ein anderer Skill
494
+ * gleichen Namens, ein fremder Eintrag oder ein Link, der zu einem anderen Bundle gehört. Derselbe
495
+ * Skill aus derselben Quelle ist kein Konflikt, er wird als Mitglied übernommen. Abbruch wie bei
496
+ * `BundleMitgliederFehlen`, die Meldung listet alle Konflikte.
497
+ */
498
+ export class BundleNamenskonflikte extends Error {
499
+ bundleName;
500
+ ziel;
501
+ konflikte;
502
+ constructor(bundleName, ziel, konflikte) {
503
+ const zeilen = konflikte.map((k) => ` ${k.name}: ${k.grund}`).join('\n');
504
+ super(`Bundle „${bundleName}" lässt sich in ${zielSchluessel(ziel)} nicht anlegen, nichts wurde verlinkt:\n${zeilen}`);
505
+ this.bundleName = bundleName;
506
+ this.ziel = ziel;
507
+ this.konflikte = konflikte;
508
+ }
509
+ }
510
+ /** Ein Bundle, das in diesem Ziel nicht installiert ist (E69): Entfernen kennt nur installierte. */
511
+ export class BundleNichtInstalliert extends Error {
512
+ bundleName;
513
+ ziel;
514
+ constructor(bundleName, ziel) {
515
+ super(`In ${zielSchluessel(ziel)} ist kein Bundle „${bundleName}" installiert`);
516
+ this.bundleName = bundleName;
517
+ this.ziel = ziel;
518
+ }
519
+ }
520
+ /**
521
+ * Eine Bundle-Definition lässt sich nicht lesen (E69): kaputtes YAML oder ein Feld außerhalb des
522
+ * dokumentierten Formats. Wie beim MCP-Katalog (E61) mit Datei, Feld und Grund in der Meldung.
523
+ */
524
+ export class BundleDateiFehler extends Error {
525
+ datei;
526
+ constructor(datei, grund) {
527
+ super(`Bundle-Datei ${datei}: ${grund}`);
528
+ this.datei = datei;
529
+ }
530
+ }
531
+ /** Der MCP-Katalog ~/.hmgr/mcp.yaml lässt sich nicht lesen (E61): kaputtes YAML oder falsches Format. */
532
+ export class McpKatalogFehler extends Error {
533
+ pfad;
534
+ constructor(pfad, grund) {
535
+ super(`MCP-Katalog ${pfad}: ${grund}`);
536
+ this.pfad = pfad;
537
+ }
538
+ }
@@ -12,9 +12,20 @@ export function ortAusSegmenten(host, segmente) {
12
12
  const besitzer = gefiltert[gefiltert.length - 2] ?? '_';
13
13
  return { host: host.toLowerCase(), besitzer, repo };
14
14
  }
15
+ /**
16
+ * Ein Windows-Laufwerkspfad (E65/B12): ein einzelner ASCII-Buchstabe, ein Doppelpunkt und dann ein
17
+ * Trenner — `C:\work\quelle` oder `C:/work/quelle`. Genau diese Form sieht sonst aus wie eine
18
+ * scp-Kurzform `host:pfad` und wurde als Host „C" mit dem Pfad „\work\quelle" gelesen. Ein Hostname
19
+ * aus einem einzelnen Buchstaben ist theoretisch erlaubt, kommt als Git-Remote aber nicht vor; der
20
+ * Windows-Pfad kommt vor.
21
+ */
22
+ export function istWindowsLaufwerkspfad(text) {
23
+ return /^[A-Za-z]:[\\/]/.test(text.trim());
24
+ }
15
25
  /**
16
26
  * Zerlegt eine Git-URL: `git@host:besitzer/repo.git`, `ssh://git@host/besitzer/repo`,
17
- * `https://host/besitzer/repo.git`, `file:///pfad`. Ein Text, der keine URL ist, gibt undefined.
27
+ * `https://host/besitzer/repo.git`, `file:///pfad`. Ein Text, der keine URL ist, gibt undefined —
28
+ * darunter jeder nackte Pfad, der lokale POSIX-Pfad ebenso wie der Windows-Laufwerkspfad (E65/B12).
18
29
  */
19
30
  export function urlOrt(url) {
20
31
  const text = url.trim().replace(/\/+$/, '');
@@ -32,6 +43,8 @@ export function urlOrt(url) {
32
43
  return ortAusSegmenten('local', segmente);
33
44
  return ortAusSegmenten(zerlegt.hostname, segmente);
34
45
  }
46
+ if (istWindowsLaufwerkspfad(text))
47
+ return undefined;
35
48
  const scp = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(text);
36
49
  if (scp)
37
50
  return ortAusSegmenten(scp[1], scp[2].split('/'));
@@ -4,6 +4,9 @@ export function kannProfile(h) {
4
4
  export function kannPlugins(h) {
5
5
  return h.plugins !== undefined;
6
6
  }
7
+ export function kannMcp(h) {
8
+ return h.mcp !== undefined;
9
+ }
7
10
  export function kannZurueckbauen(h) {
8
11
  return typeof h.zurueckbauen === 'function';
9
12
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Fassung des Dateiformats. 1 ist alles vor E69 (das Feld fehlt dort und wird beim Lesen ergänzt),
3
+ * 2 kennt `bundleInstallationen`. Bestand ohne Bundles läuft unverändert weiter: ein fehlendes Feld
4
+ * ist eine leere Liste, geschrieben wird danach in der aktuellen Fassung.
5
+ */
6
+ export const ZUSTAND_FASSUNG = 2;
1
7
  export function leererZustand() {
2
- return { quellen: [], arbeitsverzeichnisse: [], bindungen: [], aktiveHarnesses: ['claude-code'], ignorierteEintraege: [] };
8
+ return {
9
+ fassung: ZUSTAND_FASSUNG,
10
+ quellen: [],
11
+ arbeitsverzeichnisse: [],
12
+ bindungen: [],
13
+ aktiveHarnesses: ['claude-code'],
14
+ ignorierteEintraege: [],
15
+ bundleInstallationen: [],
16
+ };
3
17
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",
7
7
  "type": "module",
8
+ "main": "dist/gui/main.js",
8
9
  "bin": {
9
10
  "hmgr": "dist/cli/main.js"
10
11
  },
@@ -21,22 +22,78 @@
21
22
  "build": "tsc -p tsconfig.build.json && node scripts/copy-gui-assets.mjs",
22
23
  "gui": "npm run build && electron dist/gui/main.js",
23
24
  "gui:smoke": "npm run build && node scripts/gui-smoke-test.ts",
25
+ "gui:app": "npm run build && electron-builder --mac --arm64",
26
+ "gui:app:win": "npm run build && electron-builder --win --x64",
27
+ "gui:install": "npm run build && electron-builder --mac dir --arm64 && ditto release/mac-arm64/hmgr.app /Applications/hmgr.app",
24
28
  "test": "vitest run",
25
29
  "test:watch": "vitest",
26
30
  "type-check": "tsc --noEmit",
27
31
  "prepack": "npm run build"
28
32
  },
29
33
  "dependencies": {
34
+ "cross-spawn": "^7.0.6",
30
35
  "yaml": "^2.8.0"
31
36
  },
32
37
  "devDependencies": {
38
+ "@types/cross-spawn": "^6.0.6",
33
39
  "@types/node": "^24.0.0",
34
40
  "electron": "^44.0.0",
41
+ "electron-builder": "^26.15.3",
35
42
  "typescript": "^5.9.0",
36
43
  "vitest": "^3.2.0"
37
44
  },
38
45
  "repository": {
39
46
  "type": "git",
40
- "url": "git+https://gitea.timkaufmann.de/tim/hmgr.git"
47
+ "url": "git+https://gitea.timkaufmann.de/taquiri/hmgr.git"
48
+ },
49
+ "build": {
50
+ "appId": "de.timkaufmann.hmgr",
51
+ "productName": "hmgr",
52
+ "directories": {
53
+ "output": "release",
54
+ "buildResources": "build"
55
+ },
56
+ "files": [
57
+ "dist/**/*",
58
+ "package.json"
59
+ ],
60
+ "asar": true,
61
+ "mac": {
62
+ "category": "public.app-category.developer-tools",
63
+ "icon": "build/icon.icns",
64
+ "target": [
65
+ {
66
+ "target": "dir",
67
+ "arch": [
68
+ "arm64"
69
+ ]
70
+ },
71
+ {
72
+ "target": "zip",
73
+ "arch": [
74
+ "arm64"
75
+ ]
76
+ }
77
+ ],
78
+ "identity": null
79
+ },
80
+ "win": {
81
+ "icon": "build/icon.ico",
82
+ "target": [
83
+ {
84
+ "target": "nsis",
85
+ "arch": [
86
+ "x64"
87
+ ]
88
+ }
89
+ ]
90
+ },
91
+ "nsis": {
92
+ "oneClick": false,
93
+ "perMachine": false,
94
+ "allowElevation": false,
95
+ "allowToChangeInstallationDirectory": true,
96
+ "artifactName": "${productName}-Setup-${version}-${arch}.${ext}"
97
+ }
41
98
  }
42
99
  }