hmgr 0.0.49 → 0.0.51

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.
@@ -2,13 +2,14 @@
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { mkdir, readFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { bundleSchluessel, pfadVereinheitlichen, zielSchluessel } from "../kern/modell.js";
5
6
  import { ZUSTAND_FASSUNG, leererZustand, } from "../ports/zustand.js";
6
7
  import { atomarSchreiben } from "./atomar-schreiben.js";
7
8
  import { ohneBom } from "./datei-text.js";
8
9
  /** Der gelesene Text als Zustand. Fehlende Felder bekommen den Wert, der dem Bestand entspricht. */
9
- function deuten(roh) {
10
+ function deuten(roh, plattform) {
10
11
  const daten = JSON.parse(ohneBom(roh));
11
- return {
12
+ const zustand = {
12
13
  // Fehlt das Feld (state.json vor E69), ist es die erste Fassung: alles ohne Bundles.
13
14
  // Geschrieben wird danach in der aktuellen Fassung, ein Rückweg ist nicht vorgesehen.
14
15
  fassung: typeof daten.fassung === 'number' ? daten.fassung : 1,
@@ -25,17 +26,48 @@ function deuten(roh) {
25
26
  // Fehlt das Feld (state.json vor E78), gilt: jeder Harness hat nur sein Standard-Homedir.
26
27
  harnessHeime: (daten.harnessHeime ?? []),
27
28
  };
29
+ return plattform === 'darwin' ? inNfc(zustand, plattform) : zustand;
30
+ }
31
+ /**
32
+ * Pfade von Arbeitsverzeichnissen und Zielen in NFC (E118). Dubletten, die dabei entstehen, werden
33
+ * zusammengelegt, der erste Eintrag gewinnt: bei den Arbeitsverzeichnissen der erste Pfad, bei
34
+ * Bindungen und Bundle-Installationen der erste Eintrag zum selben Zielschlüssel (der zweite fällt
35
+ * weg, auch mit seinem Zeigerpfad bzw. seiner Skill-Liste). Geschrieben wird erst beim nächsten Speichern.
36
+ */
37
+ function inNfc(zustand, plattform) {
38
+ const nfc = (pfad) => pfadVereinheitlichen(pfad, plattform);
39
+ const ortNfc = (ort) => (ort.art === 'arbeitsverzeichnis' ? { ...ort, pfad: nfc(ort.pfad) } : ort);
40
+ const ersteJe = (liste, schluessel) => {
41
+ const gesehen = new Set();
42
+ return liste.filter((eintrag) => {
43
+ const s = schluessel(eintrag);
44
+ if (gesehen.has(s))
45
+ return false;
46
+ gesehen.add(s);
47
+ return true;
48
+ });
49
+ };
50
+ return {
51
+ ...zustand,
52
+ arbeitsverzeichnisse: ersteJe(zustand.arbeitsverzeichnisse.map(nfc), (p) => p),
53
+ bindungen: ersteJe(zustand.bindungen.map((b) => ({ ...b, ziel: { ...b.ziel, ort: ortNfc(b.ziel.ort) } })), (b) => zielSchluessel(b.ziel)),
54
+ bundleInstallationen: ersteJe(zustand.bundleInstallationen.map((b) => ({ ...b, ort: ortNfc(b.ort) })), (b) => `${zielSchluessel({ harness: b.harness, ort: b.ort })}|${bundleSchluessel({ name: b.bundle, quelle: b.quelle })}`),
55
+ harnessHeime: zustand.harnessHeime.map((h) => (h.pfad === undefined ? h : { ...h, pfad: nfc(h.pfad) })),
56
+ };
28
57
  }
29
58
  export class ZustandJson {
30
59
  pfad;
31
- constructor(pfad) {
60
+ plattform;
61
+ /** `plattform` steht für `process.platform`; unter darwin bringt das Lesen Pfade in NFC (E118). */
62
+ constructor(pfad, plattform = process.platform) {
32
63
  this.pfad = pfad;
64
+ this.plattform = plattform;
33
65
  }
34
66
  async lesen() {
35
67
  const roh = await readFile(this.pfad, 'utf8').catch(() => undefined);
36
68
  if (roh === undefined)
37
69
  return leererZustand();
38
- return deuten(roh);
70
+ return deuten(roh, this.plattform);
39
71
  }
40
72
  /**
41
73
  * Derselbe Zustand, synchron gelesen (E78). Nötig für die Auflösung der Homedirs: sie hängt an
@@ -45,7 +77,7 @@ export class ZustandJson {
45
77
  */
46
78
  lesenSync() {
47
79
  try {
48
- return deuten(readFileSync(this.pfad, 'utf8'));
80
+ return deuten(readFileSync(this.pfad, 'utf8'), this.plattform);
49
81
  }
50
82
  catch {
51
83
  return leererZustand();
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "fassung": 1,
3
3
  "versionen": {
4
+ "0.0.51": [
5
+ "Ein gelöschter Projektordner, an dem noch ein Bundle hing, wird beim Aktualisieren nicht mehr neu angelegt. `hmgr doctor` findet solche Ordner jetzt und bietet an, sie samt ihren Bundles zu vergessen."
6
+ ],
7
+ "0.0.50": [
8
+ "Die Suche in hmgr findet jetzt auch Ordner und Skills mit Umlauten, etwa „döring“. Unter macOS legt hmgr denselben Ordner nicht mehr doppelt an, wenn er einmal aus dem Finder und einmal getippt hinzugefügt wird."
9
+ ],
4
10
  "0.0.49": [
5
11
  "MCP-Server in Projektordnern mit Umlauten im Namen (etwa „Döring“) erscheinen jetzt in hmgr. Bisher fehlten sie dort, obwohl Claude Code sie benutzte. Dasselbe gilt für Plugins in solchen Ordnern."
6
12
  ],
package/dist/cli/hilfe.js CHANGED
@@ -315,6 +315,11 @@ Liegt am Ziel schon derselbe Skill aus derselben Quelle als hmgr-Link, wird er a
315
315
  übernommen: aus Bestand wird ein Bundle, ohne erst Links von Hand zu entfernen. Alles andere unter
316
316
  demselben Namen ist ein Konflikt und wird genannt.
317
317
 
318
+ Ein Bundle in einem Arbeitsverzeichnis, das es nicht mehr gibt, legen update und sync nicht wieder
319
+ an: update übergeht es still, sync --ort meldet es als fehlgeschlagen. hmgr doctor bietet an, das
320
+ Arbeitsverzeichnis samt seinen Bundle-Installationen zu vergessen. bundle add legt ein fehlendes
321
+ Verzeichnis weiterhin an, bundle remove trägt kein Arbeitsverzeichnis ein.
322
+
318
323
  hmgr unlink <skill> auf ein einzelnes Mitglied nimmt es aus dem Bundle heraus: nur dieser Link
319
324
  verschwindet, das Bundle bleibt im Ziel und merkt sich den Skill als herausgenommen. sync und
320
325
  update legen ihn nicht wieder an, neue Mitglieder der Definition ergänzen sie weiter. Zurück kommt
@@ -617,7 +622,9 @@ Prüft, ob jedes gemerkte Arbeitsverzeichnis (hmgr dir add) noch auf der Platte
617
622
  umbenanntes oder woanders neu geklontes Projekt hinterlässt sonst für immer einen toten Pfad im
618
623
  Zustand, den nur hmgr dir forget von Hand entfernt. Bei einem Fund fragt doctor wie hmgr update
619
624
  einmal für alle Funde, ob sie vergessen werden sollen; ohne Bestätigung bleiben sie gemeldet, aber
620
- gemerkt. Anders als update holt doctor nichts aus dem Netz.
625
+ gemerkt. Geprüft werden auch Arbeitsverzeichnisse, an denen nur Bundles oder Bindungen hängen
626
+ (E119); vergessen nimmt dann auch deren Bundle-Installationen aus dem Zustand. Anders als update
627
+ holt doctor nichts aus dem Netz.
621
628
 
622
629
  Dazu prüft doctor Dateien, die eine Quelle mit SOPS verschlüsselt (.gitattributes mit
623
630
  filter=sops_dotenv, sops_yaml oder sops_generic) und die noch verschlüsselt im Checkout liegen,
package/dist/cli/main.js CHANGED
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { checkoutGruppen, quellenAnzeigeName, sammelZeile, sopsGrundText, sopsZeile, verlaufZeitpunkt, } from "./verlauf-anzeige.js";
8
8
  import { Anwendungsfaelle, pluginWeltText, } from "../kern/anwendungsfaelle.js";
9
- 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
+ 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, pfadVereinheitlichen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
10
10
  import { Agents } from "../adapter/agents.js";
11
11
  import { ClaudeCode, claudeAusfuehrer } from "../adapter/claude-code.js";
12
12
  import { Hermes } from "../adapter/hermes.js";
@@ -37,10 +37,11 @@ export function umgebungLesen(env = process.env) {
37
37
  };
38
38
  }
39
39
  export function bauen(umgebung, optionen = {}) {
40
- const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'));
41
40
  const verlauf = new VerlaufJson(umgebung.hmgrHome);
42
41
  const wurzel = path.join(umgebung.hmgrHome, 'checkouts');
43
42
  const umgebungPort = optionen.umgebung ?? new ProzessUmgebung();
43
+ // Das Lesen bringt Pfade unter macOS in NFC (E118), darum braucht der Adapter die Plattform.
44
+ const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'), umgebungPort.plattform());
44
45
  // Homedirs aus dem Zustand (E78): das primäre lässt sich umstellen (F89), dazu kommen
45
46
  // zusätzliche in einer WSL-Distro (F87). `wsl:`-Heime fallen außerhalb von Windows heraus.
46
47
  const heime = new ZustandHeime(zustand, umgebung.home, umgebungPort.plattform());
@@ -237,14 +238,15 @@ function unterkommandoPruefen(kommando, schalter) {
237
238
  /**
238
239
  * `--ort` deuten. Ein zusätzliches Homedir (E78, F87) heißt nach seiner Adresse: `--ort wsl:Ubuntu`,
239
240
  * auch in der ausgeschriebenen Form `--ort home:wsl:Ubuntu`. Beides ergibt denselben Ort, damit
240
- * niemand raten muss, ob das `home:` davor gehört.
241
+ * niemand raten muss, ob das `home:` davor gehört. Ein `dir:`-Pfad kommt unter macOS in NFC (E118),
242
+ * damit er mit den gemerkten Arbeitsverzeichnissen übereinstimmt.
241
243
  */
242
- function ortLesen(text) {
244
+ function ortLesen(text, plattform) {
243
245
  const roh = text ?? 'home';
244
246
  if (roh === 'home')
245
247
  return { art: 'home' };
246
248
  if (roh.startsWith('dir:'))
247
- return { art: 'arbeitsverzeichnis', pfad: path.resolve(roh.slice(4)) };
249
+ return { art: 'arbeitsverzeichnis', pfad: pfadVereinheitlichen(path.resolve(roh.slice(4)), plattform) };
248
250
  if (roh.startsWith('profil:'))
249
251
  return { art: 'profil', name: roh.slice(7) };
250
252
  const heim = heimAdresseZerlegen(roh.startsWith('home:') ? roh.slice(5) : roh);
@@ -479,9 +481,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
479
481
  if (skill === undefined)
480
482
  throw new Error(`${kommando} braucht einen Skill-Namen, etwa hmgr ${kommando} <skill>. ` +
481
483
  'hmgr status zeigt die verlinkten Skills, hmgr source add <url|pfad> die Skills einer Quelle.');
482
- const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
484
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
483
485
  const lokal = flaggen.has('local') || flaggen.has('lokal');
484
- if (ziel.ort.art === 'arbeitsverzeichnis') {
486
+ // E119: unlink trägt ein Verzeichnis, das es nicht mehr gibt, nicht neu ein.
487
+ if (ziel.ort.art === 'arbeitsverzeichnis' &&
488
+ (kommando === 'link' || (await ports.zeiger.verzeichnisExistiert(ziel.ort.pfad)))) {
485
489
  // Verzeichnis merken und binden, damit status es wiederfindet und der Zeiger bekannt ist (E21).
486
490
  const composer = werte.get('composer');
487
491
  const verz = composer === undefined
@@ -520,7 +524,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
520
524
  if (kommando === 'adopt') {
521
525
  const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'adopt');
522
526
  restPruefen(rest, 1, 'adopt');
523
- const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
527
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
524
528
  const probelauf = flaggen.has('probelauf');
525
529
  const name = rest[0];
526
530
  if (ziel.ort.art === 'arbeitsverzeichnis' && !probelauf) {
@@ -586,7 +590,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
586
590
  const name = rest[1];
587
591
  if (name === undefined)
588
592
  throw new Error(`plugin ${unterkommando} braucht einen Plugin-Namen. hmgr plugin list zeigt die Plugins der bekannten Quellen.`);
589
- const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
593
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
590
594
  const lokal = flaggen.has('local') || flaggen.has('lokal');
591
595
  if (ziel.ort.art === 'arbeitsverzeichnis') {
592
596
  const composer = werte.get('composer');
@@ -654,8 +658,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
654
658
  const name = rest[1];
655
659
  if (name === undefined && unterkommando !== 'sync')
656
660
  throw new Error(`bundle ${unterkommando} braucht einen Bundle-Namen. hmgr bundle list zeigt die Bundles der bekannten Quellen.`);
657
- const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
658
- if (ziel.ort.art === 'arbeitsverzeichnis') {
661
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
662
+ if (unterkommando !== 'remove' && ziel.ort.art === 'arbeitsverzeichnis') {
659
663
  // Wie bei link: Verzeichnis merken und binden, damit status es wiederfindet (E21).
660
664
  const composer = werte.get('composer');
661
665
  const verz = composer === undefined
@@ -742,7 +746,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
742
746
  const name = rest[1];
743
747
  if (name === undefined)
744
748
  throw new Error(`mcp ${unterkommando} braucht einen MCP-Namen. hmgr mcp list zeigt die Katalog-Einträge.`);
745
- const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
749
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
746
750
  // Übernehmen (E64): der Name muss nicht im Katalog stehen, der Block kommt aus der geteilten Datei.
747
751
  if (unterkommando === 'migrate') {
748
752
  const ergebnis = await anwendungsfaelle.mcpUebernehmen({ name, ziel });
@@ -25,6 +25,9 @@
25
25
  var PLATTFORM = '';
26
26
  /** Trenner einer Plattform, wie `process.platform` sie nennt. Rein, nur für die Anzeige. */
27
27
  function plattformTrenner(plattform) { return plattform === 'win32' ? '\\' : '/'; }
28
+ /** Vergleichsform für alle Suchfelder (E118): NFC und kleingeschrieben. Pfade aus Finder oder
29
+ Nextcloud kommen oft in NFD (Umlaut als Buchstabe plus Punkte), getippt wird NFC. */
30
+ function suchform(text) { return String(text).normalize('NFC').toLowerCase(); }
28
31
  /**
29
32
  * Kürzt einen absoluten Pfad auf ~, wenn er mit dem Home-Verzeichnis beginnt. Grenze ist einer
30
33
  * der beiden Trenner (E68): unter Windows steht dort „\", und der Rest des Pfads bleibt wörtlich
@@ -1651,17 +1654,17 @@
1651
1654
  Suche nach „~/.claude" nichts, weil im rohen Pfad der volle Home-Pfad steht. In
1652
1655
  Kleinschreibung, leerer Suchbegriff heißt „alles". */
1653
1656
  function zielKartePasstZuSuche(z) {
1654
- var s = zielsuche.trim().toLowerCase();
1657
+ var s = suchform(zielsuche.trim());
1655
1658
  if (!s) return true;
1656
- var text = (zielTitel(z) + ' ' + (z.verzeichnis || '') + ' ' + pfadKurz(z.verzeichnis || '')).toLowerCase();
1659
+ var text = suchform(zielTitel(z) + ' ' + (z.verzeichnis || '') + ' ' + pfadKurz(z.verzeichnis || ''));
1657
1660
  return text.indexOf(s) > -1;
1658
1661
  }
1659
1662
  /** Ob eine Verzeichnis-Karte zum aktuellen Suchbegriff passt (E84 Teil 2): voller Pfad oder
1660
1663
  gekürzte Schreibweise (pfadKurz). */
1661
1664
  function verzKartePasstZuSuche(pfad) {
1662
- var s = zielsuche.trim().toLowerCase();
1665
+ var s = suchform(zielsuche.trim());
1663
1666
  if (!s) return true;
1664
- return pfad.toLowerCase().indexOf(s) > -1 || pfadKurz(pfad).toLowerCase().indexOf(s) > -1;
1667
+ return suchform(pfad).indexOf(s) > -1 || suchform(pfadKurz(pfad)).indexOf(s) > -1;
1665
1668
  }
1666
1669
 
1667
1670
  function baueZiele() {
@@ -2334,11 +2337,11 @@
2334
2337
  }
2335
2338
 
2336
2339
  function filtereSkills() {
2337
- var suche = $('suchfeld').value.trim().toLowerCase();
2340
+ var suche = suchform($('suchfeld').value.trim());
2338
2341
  var sichtbar = 0;
2339
2342
  document.querySelectorAll('#katalog .zeile').forEach(function (li) {
2340
2343
  var skill = SKILLS[+li.dataset.index];
2341
- var passtSuche = !suche || skill.name.toLowerCase().indexOf(suche) > -1 || skill.pfad.toLowerCase().indexOf(suche) > -1;
2344
+ var passtSuche = !suche || suchform(skill.name).indexOf(suche) > -1 || suchform(skill.pfad).indexOf(suche) > -1;
2342
2345
  var passtFilter = quellenFilter === 'alle' || skill.quelleSchluessel === quellenFilter;
2343
2346
  var an = passtSuche && passtFilter;
2344
2347
  li.hidden = !an;
@@ -2464,15 +2467,15 @@
2464
2467
  $('dsk-bilanz').textContent = zahlwort(n, 'ausgewählt', 'ausgewählt');
2465
2468
  }
2466
2469
  function filtereDskListe() {
2467
- var suche = $('dsk-suche').value.trim().toLowerCase();
2470
+ var suche = suchform($('dsk-suche').value.trim());
2468
2471
  document.querySelectorAll('#dsk-liste .wahlzeile').forEach(function (label) {
2469
2472
  var skill = SKILLS[+label.dataset.index];
2470
- label.hidden = !!suche && skill.name.toLowerCase().indexOf(suche) === -1;
2473
+ label.hidden = !!suche && suchform(skill.name).indexOf(suche) === -1;
2471
2474
  });
2472
2475
  // Die Suche gilt auch für Bundles (E69): sonst bliebe die Sektion bei jeder Eingabe voll stehen.
2473
2476
  document.querySelectorAll('#dsk-bundles .wahlzeile').forEach(function (label) {
2474
2477
  var bundle = BUNDLES[+label.dataset.index];
2475
- label.hidden = !!suche && bundle.name.toLowerCase().indexOf(suche) === -1;
2478
+ label.hidden = !!suche && suchform(bundle.name).indexOf(suche) === -1;
2476
2479
  });
2477
2480
  }
2478
2481
 
@@ -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, wslDistroVonOrt, wslUncBilden, 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, pfadVereinheitlichen, 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";
@@ -199,6 +199,10 @@ export class Anwendungsfaelle {
199
199
  static heimeAufraeumen(zustand) {
200
200
  zustand.harnessHeime = zustand.harnessHeime.filter((h) => (h.pfad !== undefined && h.pfad.length > 0) || h.zusaetzliche.length > 0);
201
201
  }
202
+ /** Verzeichnispfad in der gemerkten Schreibweise (E118): unter macOS NFC, sonst unverändert. */
203
+ pfadNfc(pfad) {
204
+ return pfadVereinheitlichen(pfad, this.p.umgebung.plattform());
205
+ }
202
206
  /**
203
207
  * Primäres Homedir umstellen (E78, F89): hmgr merkt sich den neuen Pfad und zeigt ab dann ihn.
204
208
  * Was am alten Ort liegt, bleibt unberührt liegen — dasselbe Verhalten wie beim Deaktivieren
@@ -207,7 +211,7 @@ export class Anwendungsfaelle {
207
211
  */
208
212
  async harnessHeimSetzen(harnessName, pfad) {
209
213
  this.harness(harnessName); // wirft HarnessTypUnbekannt bei unbekanntem Namen
210
- const aufgeloest = path.resolve(pfad);
214
+ const aufgeloest = this.pfadNfc(path.resolve(pfad));
211
215
  const zustand = await this.p.zustand.lesen();
212
216
  Anwendungsfaelle.heimEintrag(zustand, harnessName).pfad = aufgeloest;
213
217
  await this.p.zustand.schreiben(zustand);
@@ -1275,6 +1279,21 @@ export class Anwendungsfaelle {
1275
1279
  for (const installation of installationen) {
1276
1280
  const ziel = Anwendungsfaelle.installationZiel(installation);
1277
1281
  const kopf = { ziel, bundle: installation.bundle, quelle: installation.quelle };
1282
+ // E119: Ein Arbeitsverzeichnis, das es nicht mehr gibt, legt der Abgleich nicht neu an. Ohne
1283
+ // genanntes Ziel übergeht er es still (die Meldung übernimmt doctor), mit Ziel ist es ein Fehlschlag.
1284
+ if (ziel.ort.art === 'arbeitsverzeichnis' && !(await this.p.zeiger.verzeichnisExistiert(ziel.ort.pfad))) {
1285
+ if (args?.ziel === undefined)
1286
+ continue;
1287
+ ergebnis.push({
1288
+ ...kopf,
1289
+ ergebnis: 'fehlgeschlagen',
1290
+ gelegt: [],
1291
+ entfernt: [],
1292
+ neueQuellen: [],
1293
+ fehler: `Das Arbeitsverzeichnis ${ziel.ort.pfad} gibt es nicht mehr. hmgr doctor bietet an, es zu vergessen.`,
1294
+ });
1295
+ continue;
1296
+ }
1278
1297
  try {
1279
1298
  const harness = await this.harnessGeprueft(ziel.harness);
1280
1299
  // Die Definition liegt in der Quelle: ohne Nachziehen bliebe eine Änderung unsichtbar,
@@ -1330,7 +1349,7 @@ export class Anwendungsfaelle {
1330
1349
  * es sei denn, der Nutzer nennt eine composer.json. Nie Zufall.
1331
1350
  */
1332
1351
  async arbeitsverzeichnisHinzufuegen(pfad, composerPfad) {
1333
- const verzeichnis = path.resolve(pfad);
1352
+ const verzeichnis = this.pfadNfc(path.resolve(pfad));
1334
1353
  let zeigerPfad;
1335
1354
  let hinweis;
1336
1355
  // Eine schon gemerkte Bindung gewinnt, sonst würde ein zweiter Aufruf erneut mehrdeutig werden.
@@ -1375,10 +1394,11 @@ export class Anwendungsfaelle {
1375
1394
  }
1376
1395
  /** Arbeitsverzeichnis vergessen: Verzeichnis und Bindungen weg, die Links bleiben liegen (E26). */
1377
1396
  async arbeitsverzeichnisVergessen(pfad) {
1378
- const verzeichnis = path.resolve(pfad);
1397
+ const verzeichnis = this.pfadNfc(path.resolve(pfad));
1379
1398
  const zustand = await this.p.zustand.lesen();
1380
1399
  zustand.arbeitsverzeichnisse = zustand.arbeitsverzeichnisse.filter((p) => p !== verzeichnis);
1381
1400
  zustand.bindungen = zustand.bindungen.filter((b) => !(b.ziel.ort.art === 'arbeitsverzeichnis' && b.ziel.ort.pfad === verzeichnis));
1401
+ zustand.bundleInstallationen = zustand.bundleInstallationen.filter((i) => !(i.ort.art === 'arbeitsverzeichnis' && i.ort.pfad === verzeichnis));
1382
1402
  await this.p.zustand.schreiben(zustand);
1383
1403
  }
1384
1404
  /**
@@ -1402,7 +1422,15 @@ export class Anwendungsfaelle {
1402
1422
  async doctorPruefen(rueckfragen) {
1403
1423
  const zustand = await this.p.zustand.lesen();
1404
1424
  const fehlend = [];
1405
- for (const pfad of zustand.arbeitsverzeichnisse)
1425
+ // E119: auch Pfade, an denen nur Bindungen oder Bundle-Installationen hängen.
1426
+ const pfade = new Set(zustand.arbeitsverzeichnisse);
1427
+ for (const b of zustand.bindungen)
1428
+ if (b.ziel.ort.art === 'arbeitsverzeichnis')
1429
+ pfade.add(b.ziel.ort.pfad);
1430
+ for (const i of zustand.bundleInstallationen)
1431
+ if (i.ort.art === 'arbeitsverzeichnis')
1432
+ pfade.add(i.ort.pfad);
1433
+ for (const pfad of pfade)
1406
1434
  if (!(await this.p.zeiger.verzeichnisExistiert(pfad)))
1407
1435
  fehlend.push(pfad);
1408
1436
  let vergessen = [];
@@ -1959,7 +1987,7 @@ export class Anwendungsfaelle {
1959
1987
  * fehlende Links legen, Abweichungen melden. Ohne Zeiger oder ohne Harness passiert nichts.
1960
1988
  */
1961
1989
  async abgleich(pfad) {
1962
- const verzeichnis = path.resolve(pfad ?? process.cwd());
1990
+ const verzeichnis = this.pfadNfc(path.resolve(pfad ?? process.cwd()));
1963
1991
  const ergebnis = { gelegt: [], vorhanden: [], abweichend: [], fehler: [], hinweise: [] };
1964
1992
  const zustand = await this.p.zustand.lesen();
1965
1993
  // Zeiger nach E26: erst die gemerkte Bindung, dann die Suche im Verzeichnis.
@@ -1,5 +1,14 @@
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
+ * Pfad eines Arbeitsverzeichnisses oder Ziels in der Schreibweise, die hmgr merkt (E118): unter
5
+ * macOS (APFS) sind NFC und NFD desselben Ordnernamens derselbe Ordner, `realpath` gibt aber die
6
+ * übergebene Schreibweise zurück; also immer NFC. Anderswo sind beide Schreibweisen verschiedene
7
+ * Namen, der Pfad bleibt, wie er ist. `plattform` ist `process.platform`, im Kern aus der Umgebung.
8
+ */
9
+ export function pfadVereinheitlichen(pfad, plattform) {
10
+ return plattform === 'darwin' ? pfad.normalize('NFC') : pfad;
11
+ }
3
12
  /**
4
13
  * Identität eines Bundles (E69): Quelle der Definition plus Name. Zwei Quellen dürfen denselben
5
14
  * Bundle-Namen führen, mehrdeutig wird es erst beim Suchen, wie bei Skills (E12).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",