hmgr 0.0.42 → 0.0.43

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,29 @@
1
+ // Signaldatei von der CLI an eine laufende GUI (E110): `<hmgrHome>/gui-signal.json`. Die CLI
2
+ // schreibt sie nach `dir add` und Verwandten, die GUI beobachtet das Verzeichnis (src/gui/signal.ts).
3
+ // Kein Port: der Kern kennt das Signal nicht, die Entscheidung „Signal ja/nein" fällt in der CLI.
4
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { ohneBom } from "./datei-text.js";
7
+ export const SIGNAL_DATEI = 'gui-signal.json';
8
+ /** Atomar: Temp-Datei im selben Verzeichnis, dann rename darüber. */
9
+ export async function signalSchreiben(hmgrHome, signal) {
10
+ await mkdir(hmgrHome, { recursive: true });
11
+ const ziel = path.join(hmgrHome, SIGNAL_DATEI);
12
+ const temp = path.join(hmgrHome, `${SIGNAL_DATEI}.${process.pid}.tmp`);
13
+ await writeFile(temp, `${JSON.stringify(signal)}\n`);
14
+ await rename(temp, ziel);
15
+ }
16
+ /** `undefined` bei fehlender, kaputter oder unbekannt geformter Datei. */
17
+ export async function signalLesen(hmgrHome) {
18
+ try {
19
+ const roh = await readFile(path.join(hmgrHome, SIGNAL_DATEI), 'utf8');
20
+ const daten = JSON.parse(ohneBom(roh));
21
+ if (daten.art !== 'verzeichnis-zeigen' || typeof daten.pfad !== 'string' || typeof daten.zeit !== 'number') {
22
+ return undefined;
23
+ }
24
+ return { art: daten.art, pfad: daten.pfad, zeit: daten.zeit };
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "fassung": 1,
3
3
  "versionen": {
4
+ "0.0.43": [
5
+ "Fügst du im Terminal ein Arbeitsverzeichnis hinzu (hmgr dir add oder link mit einem neuen Verzeichnis), kommt ein offenes hmgr-Fenster nach vorn und zeigt das Verzeichnis sofort an."
6
+ ],
4
7
  "0.0.42": [
5
8
  "Der Verlauf meldet jetzt auch neue und entfernte Skills einer Quelle, nicht nur geänderte.",
6
9
  "Verlauf und „hmgr update“ sind leichter zu lesen: Quellen erscheinen mit ihrem Namen, darunter Neu, Entfernt und Geändert, ohne Commit-Kürzel. Quellen ohne Änderung an deinen Skills stehen gesammelt in einer Zeile am Ende."
package/dist/cli/hilfe.js CHANGED
@@ -104,6 +104,10 @@ Plugins des Vorhabens stehen. Ohne Zeiger bleibt das Verzeichnis gemerkt und jed
104
104
  (E26). dir forget löst Bindung und Eintrag, die gelegten Links bleiben liegen. Gebunden wird nur für
105
105
  Harnesses, die hmgr verwaltet: standardmäßig nur claude-code (E38).
106
106
 
107
+ Läuft die hmgr-GUI, kommt sie bei dir add nach vorn und zeigt das Verzeichnis (E110). Dasselbe
108
+ passiert bei link, unlink, adopt, bundle add und plugin add mit --ort dir:<pfad>, wenn das Verzeichnis
109
+ dabei neu gemerkt wird. Über SSH bleibt die GUI in Ruhe, und läuft keine GUI, passiert nichts.
110
+
107
111
  Ein Arbeitsverzeichnis unter Windows, das in einer WSL-Distro liegt (\\\\wsl$\\<distro>\\… oder
108
112
  \\\\wsl.localhost\\<distro>\\…, E96), bedient hmgr über die Bridge dieser Distro, wie ein Homedir in
109
113
  WSL: der Link liegt im Linux-Dateisystem und zeigt auf den Windows-Checkout (/mnt/c). MCP-Server
@@ -135,6 +139,9 @@ nur für ein Arbeitsverzeichnis mit composer.json, an Home und Profil bleibt jed
135
139
  Das Schreiben in einen Zeiger ist gesperrt, bis das Composer-Thema entschieden ist (E57): an einem
136
140
  Arbeitsverzeichnis mit Zeiger bricht link ohne --local ab, der Link wird nicht gelegt.
137
141
 
142
+ Mit --ort dir:<pfad> kommt eine laufende hmgr-GUI nach vorn und zeigt das Verzeichnis, wenn es dabei
143
+ neu gemerkt wird (nicht per SSH, E110).
144
+
138
145
  Schalter:
139
146
  --harness <name> claude-code, hermes oder agents, Standard claude-code
140
147
  --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
package/dist/cli/main.js CHANGED
@@ -21,6 +21,7 @@ import { paketVersion } from "../adapter/paket-version.js";
21
21
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
22
22
  import { TerminalRueckfrage } from "../adapter/terminal.js";
23
23
  import { ProzessUmgebung } from "../adapter/umgebung.js";
24
+ import { signalSchreiben } from "../adapter/gui-signal.js";
24
25
  import { ZustandJson } from "../adapter/zustand-json.js";
25
26
  import { VerlaufJson } from "../adapter/verlauf-json.js";
26
27
  import { AenderungenJson } from "../adapter/aenderungen-json.js";
@@ -259,6 +260,21 @@ const UEBERNAHME_GRUND = {
259
260
  'lokale-aenderungen': 'Ordner weicht von der Quelle ab',
260
261
  'nicht-pruefbar': 'hmgr kann nicht prüfen, ob der Ordner der Quelle entspricht',
261
262
  };
263
+ /**
264
+ * E110: gibt einer laufenden GUI Bescheid, dass dieses Verzeichnis gezeigt werden soll. Nie bei
265
+ * SSH-Aufrufen, nie ein Grund zu scheitern.
266
+ */
267
+ async function guiBenachrichtigen(umgebung, ports, pfad) {
268
+ try {
269
+ const variablen = ports.umgebung.variablen();
270
+ if ((variablen.SSH_CONNECTION ?? '') !== '' || (variablen.SSH_TTY ?? '') !== '')
271
+ return;
272
+ await signalSchreiben(umgebung.hmgrHome, { art: 'verzeichnis-zeigen', pfad, zeit: Date.now() });
273
+ }
274
+ catch {
275
+ // Das Signal ist ein Zusatz; das Kommando selbst hat funktioniert.
276
+ }
277
+ }
262
278
  export async function ausfuehren(argv, umgebung, optionen = {}) {
263
279
  const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
264
280
  const kommando = argv[0];
@@ -426,6 +442,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
426
442
  const ergebnis = composer === undefined
427
443
  ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)
428
444
  : await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad, composer);
445
+ await guiBenachrichtigen(umgebung, ports, ergebnis.pfad);
429
446
  console.log(`Arbeitsverzeichnis ${ergebnis.pfad}`);
430
447
  if (ergebnis.zeigerPfad !== undefined)
431
448
  console.log(` Zeiger ${ergebnis.zeigerPfad}`);
@@ -465,10 +482,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
465
482
  if (ziel.ort.art === 'arbeitsverzeichnis') {
466
483
  // Verzeichnis merken und binden, damit status es wiederfindet und der Zeiger bekannt ist (E21).
467
484
  const composer = werte.get('composer');
468
- if (composer === undefined)
469
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
470
- else
471
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
485
+ const verz = composer === undefined
486
+ ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad)
487
+ : await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
488
+ if (verz.neu)
489
+ await guiBenachrichtigen(umgebung, ports, verz.pfad);
472
490
  }
473
491
  if (kommando === 'unlink') {
474
492
  const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
@@ -504,10 +522,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
504
522
  const name = rest[0];
505
523
  if (ziel.ort.art === 'arbeitsverzeichnis' && !probelauf) {
506
524
  const composer = werte.get('composer');
507
- if (composer === undefined)
508
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
509
- else
510
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
525
+ const verz = composer === undefined
526
+ ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad)
527
+ : await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
528
+ if (verz.neu)
529
+ await guiBenachrichtigen(umgebung, ports, verz.pfad);
511
530
  }
512
531
  const ergebnis = await anwendungsfaelle.uebernehmen({
513
532
  ziel,
@@ -568,10 +587,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
568
587
  const lokal = flaggen.has('local') || flaggen.has('lokal');
569
588
  if (ziel.ort.art === 'arbeitsverzeichnis') {
570
589
  const composer = werte.get('composer');
571
- if (composer === undefined)
572
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
573
- else
574
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
590
+ const verz = composer === undefined
591
+ ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad)
592
+ : await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
593
+ if (verz.neu)
594
+ await guiBenachrichtigen(umgebung, ports, verz.pfad);
575
595
  }
576
596
  const quelle = werte.get('quelle');
577
597
  const ergebnis = unterkommando === 'add'
@@ -627,10 +647,11 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
627
647
  if (ziel.ort.art === 'arbeitsverzeichnis') {
628
648
  // Wie bei link: Verzeichnis merken und binden, damit status es wiederfindet (E21).
629
649
  const composer = werte.get('composer');
630
- if (composer === undefined)
631
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
632
- else
633
- await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
650
+ const verz = composer === undefined
651
+ ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad)
652
+ : await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
653
+ if (verz.neu)
654
+ await guiBenachrichtigen(umgebung, ports, verz.pfad);
634
655
  }
635
656
  if (unterkommando === 'add') {
636
657
  const quelle = werte.get('quelle');
package/dist/gui/main.js CHANGED
@@ -21,6 +21,8 @@ import { iconFuerQuelle } from "./icons.js";
21
21
  import { entscheideOeffnen } from "./oeffnen.js";
22
22
  import { mcpUebernahmeSerialisieren, mcpUebersichtSerialisieren, mcpWirkungSerialisieren } from "./mcp-ansicht.js";
23
23
  import { beobachten } from "./beobachter.js";
24
+ import { signalSchreiben } from "../adapter/gui-signal.js";
25
+ import { signalBeobachten } from "./signal.js";
24
26
  import { loginPathUebernehmen } from "./login-pfad.js";
25
27
  import { hmgrCliVersion } from "./hmgr-cli.js";
26
28
  import { cliUpdateHinweis } from "../adapter/npm-registry.js";
@@ -505,6 +507,7 @@ function neuLadenAnstossenFabrik(minAbstandMs) {
505
507
  }
506
508
  const neuLadenAnstossen = neuLadenAnstossenFabrik(FOKUS_RELOAD_MIN_ABSTAND_MS);
507
509
  let beobachter;
510
+ let signalBeobachter;
508
511
  function fensterOeffnen() {
509
512
  hauptfenster = new BrowserWindow({
510
513
  width: 1280,
@@ -1367,6 +1370,63 @@ function fensterOeffnen() {
1367
1370
  await new Promise((r) => setTimeout(r, 800));
1368
1371
  const auszug = await fenster.webContents.executeJavaScript(NEUSTART_AUSZUG_SKRIPT);
1369
1372
  console.log(`[smoke] Neustart-Modal: ${String(auszug)}`);
1373
+ })
1374
+ // E110: CLI-Signal gegen die laufende GUI. Das Signal schreibt dieser Main-Prozess in das
1375
+ // Wegwerf-Home (der Renderer kann keine Dateien schreiben). Fall 1: ohne Dialog springt die
1376
+ // GUI aus einer anderen Ansicht zur Karte. Fall 2: mit offenem Dialog bleibt alles stehen,
1377
+ // erst nach dem Schließen folgt der Sprung. Läuft nach der Neustart-Modal-Prüfung, weil der Fall
1378
+ // offene Dialoge schließt.
1379
+ .then(async () => {
1380
+ const pfade = (process.env.HMGR_GUI_SMOKE_SIGNAL_PFADE ?? '').split('\n').filter((p) => p !== '');
1381
+ if (pfade.length < 2)
1382
+ return;
1383
+ const [pfad1, pfad2] = pfade;
1384
+ const js = (code) => fenster.webContents.executeJavaScript(code);
1385
+ const karteJs = (pfad) => `(function () {
1386
+ var schluessel = ${JSON.stringify(`dir:${pfad}`)};
1387
+ var karte = Array.from(document.querySelectorAll('#zielliste .ziel.verzeichnis'))
1388
+ .find(function (k) { return k.dataset.verzKey === schluessel; });
1389
+ return {
1390
+ karte: karte !== undefined,
1391
+ zieleSichtbar: document.getElementById('ansicht-ziele').hidden === false,
1392
+ aufgeklappt: karte?.querySelector('.zielkopf')?.getAttribute('aria-expanded') ?? null,
1393
+ };
1394
+ })()`;
1395
+ const warteAufSprung = async (pfad) => {
1396
+ let stand = null;
1397
+ for (let ms = 0; ms < 4000; ms += 100) {
1398
+ await new Promise((r) => setTimeout(r, 100));
1399
+ stand = await js(karteJs(pfad));
1400
+ const k = stand;
1401
+ if (k.zieleSichtbar && k.aufgeklappt === 'true')
1402
+ break;
1403
+ }
1404
+ return stand;
1405
+ };
1406
+ const ergebnis = {};
1407
+ try {
1408
+ await js(`new Promise(function (r) { var n = 0; (function w() { if (document.querySelector('dialog[open]') || ++n > 30) r(); else setTimeout(w, 50); })(); })`);
1409
+ await js(`document.querySelectorAll('dialog[open]').forEach(function (d) { d.close(); });
1410
+ document.querySelector('[data-ansicht="mcp"]').click(); true`);
1411
+ ergebnis.fall1Vorher = await js(karteJs(pfad1));
1412
+ await signalSchreiben(umgebung.hmgrHome, { art: 'verzeichnis-zeigen', pfad: pfad1, zeit: Date.now() });
1413
+ ergebnis.fall1 = await warteAufSprung(pfad1);
1414
+ // Fall 2: Dialog offen, andere Ansicht, Signal für ein anderes Verzeichnis.
1415
+ await js(`document.querySelector('[data-ansicht="mcp"]').click();
1416
+ document.getElementById('d-einstellungen').showModal(); true`);
1417
+ ergebnis.fall2Vorher = await js(karteJs(pfad2));
1418
+ await signalSchreiben(umgebung.hmgrHome, { art: 'verzeichnis-zeigen', pfad: pfad2, zeit: Date.now() });
1419
+ await new Promise((r) => setTimeout(r, 1500));
1420
+ ergebnis.fall2Offen = await js(`({ dialogOffen: document.getElementById('d-einstellungen').open === true, ` +
1421
+ `ansichtMcp: document.getElementById('ansicht-mcp').hidden === false })`);
1422
+ ergebnis.fall2NachWarten = await js(karteJs(pfad2));
1423
+ await js(`document.getElementById('d-einstellungen').close(); true`);
1424
+ ergebnis.fall2 = await warteAufSprung(pfad2);
1425
+ }
1426
+ catch (fehler) {
1427
+ ergebnis.fehler = String(fehler);
1428
+ }
1429
+ console.log(`[smoke] Signal: ${JSON.stringify(ergebnis)}`);
1370
1430
  })
1371
1431
  .finally(() => app.quit());
1372
1432
  }, 3000);
@@ -1407,6 +1467,26 @@ function fensterOeffnen() {
1407
1467
  beobachter = undefined;
1408
1468
  });
1409
1469
  }
1470
+ // CLI-Signal (E110): `hmgr dir add` und Verwandte schreiben `gui-signal.json` ins hmgr-Home. Anders
1471
+ // als der Watcher oben ohne Entprellen und ohne Drossel; läuft auch im Smoke-Test (mit dessen
1472
+ // Wegwerf-Home), dort aber ohne Fokusklau.
1473
+ {
1474
+ const fenster = hauptfenster;
1475
+ signalBeobachter = signalBeobachten(umgebung.hmgrHome, (pfad) => {
1476
+ if (fenster.isDestroyed())
1477
+ return;
1478
+ if (!smoketest) {
1479
+ fensterNachVornHolen();
1480
+ if (process.platform === 'darwin')
1481
+ app.focus({ steal: true });
1482
+ }
1483
+ fenster.webContents.send('hmgr:verzeichnisZeigen', pfad);
1484
+ });
1485
+ fenster.on('closed', () => {
1486
+ signalBeobachter?.close();
1487
+ signalBeobachter = undefined;
1488
+ });
1489
+ }
1410
1490
  void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
1411
1491
  }
1412
1492
  const { autoUpdater } = electronUpdater;
@@ -79,6 +79,11 @@ contextBridge.exposeInMainWorld('hmgr', {
79
79
  onNeuLadenAnstossen: (hoerer) => {
80
80
  ipcRenderer.on('hmgr:neuLadenAnstossen', () => hoerer());
81
81
  },
82
+ /** Signal der CLI (E110): ein Arbeitsverzeichnis soll gezeigt werden. Main hat das Fenster schon
83
+ nach vorn geholt; kein Entprellen, keine Drossel. */
84
+ onVerzeichnisZeigen: (hoerer) => {
85
+ ipcRenderer.on('hmgr:verzeichnisZeigen', (_event, pfad) => hoerer(pfad));
86
+ },
82
87
  /** Prüfung auf neue hmgr-Stände (E77), angestoßen bei jedem Aktualisieren-Lauf (E80: Start,
83
88
  Intervall, Klick). Antwortet nicht, meldet sich später über onSelbstUpdate — Netz und Spawn
84
89
  dauern länger als ein Knopfdruck. */
@@ -2804,6 +2804,7 @@
2804
2804
  knopf.classList.remove('laeuft');
2805
2805
  // Scheiterte der Lauf, bliebe sonst „Prüft …" stehen.
2806
2806
  if ($('standtext').textContent === 'Prüft …') standText();
2807
+ reloadWartendeFreigeben();
2807
2808
  }
2808
2809
  }
2809
2810
 
@@ -2883,6 +2884,21 @@
2883
2884
  // überholt. Fehler landen wie sonst über rufe() in der Meldungsleiste, kein stiller Absturz.
2884
2885
  var stillerReloadLaeuft = false;
2885
2886
  var stillerReloadVorgemerkt = false;
2887
+ // Wer auf das Ende des Neuladens wartet (E110): die Auflöser werden frei, sobald weder ein
2888
+ // stiller Reload noch ein Aktualisieren-Lauf (der mit ladeAlles endet) mehr läuft.
2889
+ var reloadWartende = [];
2890
+ function reloadWartendeFreigeben() {
2891
+ if (stillerReloadLaeuft || stillerReloadVorgemerkt || $('updatestart').disabled) return;
2892
+ var liste = reloadWartende;
2893
+ reloadWartende = [];
2894
+ liste.forEach(function (aufloeser) { aufloeser(); });
2895
+ }
2896
+ function stillerReloadAbwarten() {
2897
+ return new Promise(function (aufloeser) {
2898
+ reloadWartende.push(aufloeser);
2899
+ void stillerReload();
2900
+ });
2901
+ }
2886
2902
  async function stillerReload() {
2887
2903
  // Läuft gerade ein Aktualisieren-Lauf, wird nicht parallel geladen: er endet ohnehin mit
2888
2904
  // ladeAlles und nimmt die Änderung mit. Sonst bauten zwei Läufe gleichzeitig die Ansicht auf.
@@ -2901,11 +2917,36 @@
2901
2917
  if (stillerReloadVorgemerkt) {
2902
2918
  stillerReloadVorgemerkt = false;
2903
2919
  void stillerReload();
2920
+ } else {
2921
+ reloadWartendeFreigeben();
2904
2922
  }
2905
2923
  }
2906
2924
  }
2907
2925
  window.hmgr.onNeuLadenAnstossen(function () { void stillerReload(); });
2908
2926
 
2927
+ // CLI-Signal (E110, nach `hmgr dir add` und Verwandten): sofort neu laden, dann zur Karte des
2928
+ // Verzeichnisses springen, auch aus einer anderen Ansicht. Steht ein Dialog offen, bleibt er
2929
+ // unangetastet und der Sprung wartet, bis kein Dialog mehr offen ist; ein neueres Signal ersetzt
2930
+ // die Vormerkung. Die Meldungsleiste bleibt, wie sie ist.
2931
+ var signalPfadVorgemerkt = null;
2932
+ function signalSprungEinloesen() {
2933
+ if (signalPfadVorgemerkt === null) return;
2934
+ if (document.querySelector('dialog[open]')) return;
2935
+ var pfad = signalPfadVorgemerkt;
2936
+ signalPfadVorgemerkt = null;
2937
+ // Eine E84-Vormerkung auf denselben Pfad ist mit diesem Sprung erledigt.
2938
+ if (zielSprungVorgemerkt === pfad) zielSprungVorgemerkt = null;
2939
+ zeigeAnsicht('ziele');
2940
+ springeZuVerzeichnis(pfad);
2941
+ }
2942
+ window.hmgr.onVerzeichnisZeigen(function (pfad) {
2943
+ signalPfadVorgemerkt = pfad;
2944
+ void stillerReloadAbwarten().then(signalSprungEinloesen);
2945
+ });
2946
+ // `close` bubbelt nicht, der Capture-Listener am Dokument sieht aber jeden geschlossenen Dialog.
2947
+ // Der Aufschub um einen Takt lässt einen unmittelbar folgenden Dialog zuerst aufgehen.
2948
+ document.addEventListener('close', function () { setTimeout(signalSprungEinloesen, 0); }, true);
2949
+
2909
2950
  /**
2910
2951
  * Eine Ereigniszeile im Verlauf (E80): Marke + Text, Muster wie die frühere baueBericht-Zeile
2911
2952
  * und die CLI-Ausgabe (verlaufEreignisZeile, src/cli/main.ts). Checkouts stellt
@@ -0,0 +1,58 @@
1
+ // Beobachter für das CLI-Signal (E110), ohne Electron-Import (testbar wie beobachter.ts). Beobachtet
2
+ // wird das Verzeichnis hmgrHome mit Filter auf `gui-signal.json` (aus demselben Grund wie in
3
+ // beobachter.ts: die Datei wird per rename ersetzt, ein Watcher auf die Datei würde taub).
4
+ // Anders als beobachter.ts kein Entprellen und kein Inhaltsvergleich: jedes frische Signal zählt.
5
+ import { watch } from 'node:fs';
6
+ import { SIGNAL_DATEI, signalLesen } from "../adapter/gui-signal.js";
7
+ export function signalBeobachten(hmgrHome, callback, optionen = {}) {
8
+ const sammelnMs = optionen.sammelnMs ?? 30;
9
+ const maxAlterMs = optionen.maxAlterMs ?? 10_000;
10
+ let timer;
11
+ let geschlossen = false;
12
+ let zuletzt;
13
+ // Eine beim Start vorhandene Datei gilt als schon gesehen. Das Alter allein reicht nicht: auf macOS
14
+ // liefert fs.watch kurz nach dem Anlegen auch Events für Schreibvorgänge kurz davor.
15
+ const startStand = signalLesen(hmgrHome).then((signal) => {
16
+ if (signal !== undefined)
17
+ zuletzt = `${signal.zeit}\u0000${signal.pfad}`;
18
+ });
19
+ async function auswerten() {
20
+ await startStand;
21
+ const signal = await signalLesen(hmgrHome);
22
+ if (geschlossen || signal === undefined)
23
+ return;
24
+ if (Date.now() - signal.zeit > maxAlterMs)
25
+ return;
26
+ const schluessel = `${signal.zeit}\u0000${signal.pfad}`;
27
+ if (schluessel === zuletzt)
28
+ return;
29
+ zuletzt = schluessel;
30
+ callback(signal.pfad);
31
+ }
32
+ let watcher;
33
+ try {
34
+ watcher = watch(hmgrHome, (_ereignis, name) => {
35
+ if (geschlossen)
36
+ return;
37
+ if (name !== null && name !== SIGNAL_DATEI)
38
+ return;
39
+ if (timer !== undefined)
40
+ return; // schon ein Lesen eingeplant
41
+ timer = setTimeout(() => {
42
+ timer = undefined;
43
+ void auswerten();
44
+ }, sammelnMs);
45
+ });
46
+ }
47
+ catch {
48
+ // Verzeichnis fehlt: unbeobachtet.
49
+ }
50
+ return {
51
+ close() {
52
+ geschlossen = true;
53
+ if (timer !== undefined)
54
+ clearTimeout(timer);
55
+ watcher?.close();
56
+ },
57
+ };
58
+ }
@@ -1322,7 +1322,8 @@ export class Anwendungsfaelle {
1322
1322
  hinweis = `Kein Zeiger unter ${verzeichnis}, alles bleibt lokal (E26).`;
1323
1323
  }
1324
1324
  const zustand = await this.p.zustand.lesen();
1325
- if (!zustand.arbeitsverzeichnisse.includes(verzeichnis))
1325
+ const neu = !zustand.arbeitsverzeichnisse.includes(verzeichnis);
1326
+ if (neu)
1326
1327
  zustand.arbeitsverzeichnisse.push(verzeichnis);
1327
1328
  if (zeigerPfad !== undefined) {
1328
1329
  const gebunden = zeigerPfad;
@@ -1338,6 +1339,7 @@ export class Anwendungsfaelle {
1338
1339
  await this.p.zustand.schreiben(zustand);
1339
1340
  return {
1340
1341
  pfad: verzeichnis,
1342
+ neu,
1341
1343
  ...(zeigerPfad === undefined ? {} : { zeigerPfad }),
1342
1344
  ...(hinweis === undefined ? {} : { hinweis }),
1343
1345
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",