hmgr 0.0.47 → 0.0.49

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.
@@ -0,0 +1,33 @@
1
+ // Atomares Schreiben für Zustandsdateien (state.json, verlauf.json): Inhalt in eine Temp-Datei im
2
+ // selben Verzeichnis, dann rename über das Ziel. Ein Leser (GUI-Intervall, CLI in einer anderen
3
+ // Sitzung) sieht so immer den alten oder den neuen vollständigen Inhalt, nie eine leere oder halbe Datei.
4
+ import { randomBytes } from 'node:crypto';
5
+ import { rename, rm, writeFile } from 'node:fs/promises';
6
+ /** Unter Windows scheitert rename über eine Datei, die ein anderer Prozess gerade offen hat, mit diesen Codes. */
7
+ const WIEDERHOLBAR = new Set(['EPERM', 'EACCES', 'EBUSY']);
8
+ const VERSUCHE = 5;
9
+ async function warten(ms) {
10
+ await new Promise((r) => setTimeout(r, ms));
11
+ }
12
+ export async function atomarSchreiben(ziel, inhalt) {
13
+ const temp = `${ziel}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
14
+ try {
15
+ await writeFile(temp, inhalt);
16
+ for (let versuch = 1;; versuch++) {
17
+ try {
18
+ await rename(temp, ziel);
19
+ return;
20
+ }
21
+ catch (fehler) {
22
+ const code = fehler.code;
23
+ if (versuch >= VERSUCHE || code === undefined || !WIEDERHOLBAR.has(code))
24
+ throw fehler;
25
+ await warten(20 * versuch);
26
+ }
27
+ }
28
+ }
29
+ catch (fehler) {
30
+ await rm(temp, { force: true });
31
+ throw fehler;
32
+ }
33
+ }
@@ -54,12 +54,15 @@ export const claudeAusfuehrer = (args, cwd) => new Promise((aufloesen) => {
54
54
  });
55
55
  /**
56
56
  * Pfadvergleich für E102: abschließende Trenner zählen nicht; bei Windows-Pfaden (Laufwerk oder
57
- * UNC) sind außerdem `\\` und `/` sowie Groß-/Kleinschreibung gleich.
57
+ * UNC) sind außerdem `\\` und `/` sowie Groß-/Kleinschreibung gleich. Beide Seiten werden in NFC
58
+ * verglichen (E117), weil Claude Code Pfade zusammengesetzt führt, hmgr sie aber oft zerlegt (NFD) merkt.
58
59
  */
59
60
  export function pfadGleich(a, b) {
60
61
  const windows = (p) => /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith('\\\\');
61
62
  const norm = (p, win) => {
62
- let t = win ? p.replaceAll('\\', '/').toLowerCase() : p;
63
+ let t = p.normalize('NFC');
64
+ if (win)
65
+ t = t.replaceAll('\\', '/').toLowerCase();
63
66
  while (t.length > 1 && t.endsWith('/'))
64
67
  t = t.slice(0, -1);
65
68
  return t;
@@ -467,6 +470,9 @@ class ClaudeMcp {
467
470
  *
468
471
  * In WSL (E100) ist es die `~/.claude.json` der Distro, über die Bridge gelesen; der Schlüssel ist
469
472
  * der Linux-Pfad, zusätzlich in der Distro aufgelöst (`realpath`).
473
+ *
474
+ * Claude Code führt den Schlüssel immer in NFC, hmgr merkt Pfade oft in NFD: Probiert werden
475
+ * Pfad, aufgelöster Pfad und beide in NFC (E117).
470
476
  */
471
477
  async mcpServersAm(ort) {
472
478
  const distro = wslDistroVonOrt(ort);
@@ -476,13 +482,21 @@ class ClaudeMcp {
476
482
  if (ort.art === 'arbeitsverzeichnis') {
477
483
  const projects = objektFeld(daten, 'projects');
478
484
  const pfad = this.cliPfad(ort.pfad);
479
- const direkt = objektFeld(projects, pfad);
485
+ const gesehen = new Set();
486
+ const versuchen = (schluessel) => {
487
+ if (schluessel === undefined || gesehen.has(schluessel))
488
+ return undefined;
489
+ gesehen.add(schluessel);
490
+ const treffer = objektFeld(projects, schluessel);
491
+ return treffer === undefined ? undefined : mcpServersVon(treffer);
492
+ };
493
+ const direkt = versuchen(pfad) ?? versuchen(pfad.normalize('NFC'));
480
494
  if (direkt !== undefined)
481
- return mcpServersVon(direkt);
495
+ return direkt;
482
496
  const echt = distro === undefined
483
497
  ? aufgeloest(pfad)
484
498
  : await this.harness.bruecke(distro).aufgeloest(pfad).catch(() => undefined);
485
- return mcpServersVon(echt === undefined ? undefined : objektFeld(projects, echt));
499
+ return versuchen(echt) ?? versuchen(echt?.normalize('NFC')) ?? {};
486
500
  }
487
501
  throw new OrtNichtUnterstuetzt(this.harness.name, ort.art);
488
502
  }
@@ -1,8 +1,9 @@
1
1
  // Der Verlauf als eine JSON-Datei, Standard ~/.hmgr/verlauf.json (E80). Analog ZustandJson
2
2
  // (zustand-json.ts), aber ohne Fassungs-Migration: das Format ist neu, `fassung: 1` reicht.
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { mkdir, readFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { leererVerlauf } from "../ports/verlauf.js";
6
+ import { atomarSchreiben } from "./atomar-schreiben.js";
6
7
  import { ohneBom } from "./datei-text.js";
7
8
  /** Höchstzahl der Einträge in der Datei (E80): ein Changelog, keine Historie ohne Ende. */
8
9
  const MAX_EINTRAEGE = 20;
@@ -34,6 +35,6 @@ export class VerlaufJson {
34
35
  await mkdir(path.dirname(this.pfad), { recursive: true });
35
36
  // Neueste zuerst kappen, nicht sortieren: die Reihenfolge liegt beim Aufrufer.
36
37
  const gekappt = { gemerkt: daten.gemerkt, eintraege: daten.eintraege.slice(0, MAX_EINTRAEGE) };
37
- await writeFile(this.pfad, `${JSON.stringify({ fassung: 1, ...gekappt }, null, 2)}\n`);
38
+ await atomarSchreiben(this.pfad, `${JSON.stringify({ fassung: 1, ...gekappt }, null, 2)}\n`);
38
39
  }
39
40
  }
@@ -1,8 +1,9 @@
1
1
  // Lokaler Zustand als eine JSON-Datei, Standard ~/.hmgr/state.json (E29).
2
2
  import { readFileSync } from 'node:fs';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { mkdir, readFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { ZUSTAND_FASSUNG, leererZustand, } from "../ports/zustand.js";
6
+ import { atomarSchreiben } from "./atomar-schreiben.js";
6
7
  import { ohneBom } from "./datei-text.js";
7
8
  /** Der gelesene Text als Zustand. Fehlende Felder bekommen den Wert, der dem Bestand entspricht. */
8
9
  function deuten(roh) {
@@ -54,6 +55,6 @@ export class ZustandJson {
54
55
  await mkdir(path.dirname(this.pfad), { recursive: true });
55
56
  // Immer in der aktuellen Fassung, egal welche gelesen wurde: das Lesen hat den Bestand schon
56
57
  // auf die aktuelle Form gebracht, ein alter Vermerk in der Datei wäre danach eine Lüge.
57
- await writeFile(this.pfad, `${JSON.stringify({ ...zustand, fassung: ZUSTAND_FASSUNG }, null, 2)}\n`);
58
+ await atomarSchreiben(this.pfad, `${JSON.stringify({ ...zustand, fassung: ZUSTAND_FASSUNG }, null, 2)}\n`);
58
59
  }
59
60
  }
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "fassung": 1,
3
3
  "versionen": {
4
+ "0.0.49": [
5
+ "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
+ ],
7
+ "0.0.48": [
8
+ "Die Meldung „Unexpected end of JSON input“ taucht nicht mehr auf, wenn hmgr an mehreren Stellen gleichzeitig läuft, etwa die Oberfläche und die Kommandozeile: hmgr schreibt seinen Zustand und den Verlauf jetzt so, dass nie eine halb geschriebene Datei gelesen wird."
9
+ ],
4
10
  "0.0.47": [
5
11
  "Skills mit SOPS-verschlüsselten Dateien kommen jetzt entschlüsselt an, wenn sops, der passende Schlüssel und sops-clean.sh vorhanden sind; bereits verschlüsselt ausgecheckte Dateien repariert hmgr beim nächsten Aktualisieren. Klappt das nicht, bleibt die Datei verschlüsselt und hmgr sagt im Verlauf und in hmgr status, warum."
6
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.47",
3
+ "version": "0.0.49",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",