hmgr 0.0.5 → 0.0.6
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/README.md +10 -0
- package/dist/adapter/checkout-pfad.js +10 -2
- package/dist/adapter/claude-code.js +231 -7
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +42 -4
- package/dist/adapter/git.js +34 -8
- package/dist/adapter/hermes.js +73 -1
- package/dist/adapter/marketplace-json.js +2 -1
- package/dist/adapter/mcp-katalog.js +124 -0
- package/dist/adapter/quelle-text.js +10 -1
- package/dist/adapter/skill-md.js +2 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +2 -1
- package/dist/cli/hilfe.js +98 -0
- package/dist/cli/main.js +97 -3
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +361 -19
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +1 -1
- package/dist/gui/preload.cjs +16 -1
- package/dist/gui/renderer/index.html +96 -6
- package/dist/gui/renderer/renderer.js +654 -38
- package/dist/gui/renderer/styles.css +65 -2
- package/dist/kern/anwendungsfaelle.js +175 -4
- package/dist/kern/modell.js +141 -3
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +3 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/package.json +43 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Ermittelt den PATH einer Login-Shell des Nutzers (F47=O2). Grund: die gepackte hmgr.app startet
|
|
2
|
+
// aus dem Finder mit dem macOS-Minimal-PATH (/usr/bin:/bin:/usr/sbin:/sbin), darin fehlt z. B.
|
|
3
|
+
// ~/.local/bin mit der claude-CLI (E63). Reine Funktion, ohne Electron, nach dem Muster von
|
|
4
|
+
// oeffnen.ts: main.ts ruft sie beim Start und übernimmt nur process.env.PATH.
|
|
5
|
+
//
|
|
6
|
+
// Unter Windows unterbleibt die Ermittlung ganz (E67): dort erbt ein aus dem Startmenü gestarteter
|
|
7
|
+
// Prozess den vollen Benutzer- und System-PATH aus der Registry, es gibt keine Lücke zu schließen,
|
|
8
|
+
// und eine Login-Shell im POSIX-Sinn gibt es nicht.
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
/** Zeitgrenze für die Shell. Gemessen auf picasso: `zsh -i -l -c` braucht ~2,0 s, das Doppelte
|
|
12
|
+
als Puffer reicht; wird sie überschritten, bleibt der PATH eben unverändert. */
|
|
13
|
+
export const ZEITGRENZE_MS = 5_000;
|
|
14
|
+
/**
|
|
15
|
+
* Standard-Ausführer: die angegebene Shell, stdin zu (sonst kann sie an einem Prompt hängen),
|
|
16
|
+
* stderr verworfen. Ein Fehlschlag wird zum Code, nicht zur Ausnahme — die PATH-Ermittlung darf
|
|
17
|
+
* die App nie zum Absturz bringen.
|
|
18
|
+
*
|
|
19
|
+
* Eigener Wecker statt der `timeout`-Option von `spawn`: die killt zwar die Shell, aber `close`
|
|
20
|
+
* feuert erst, wenn auch alle Erben die stdout-Pipe losgelassen haben — eine interaktive Shell
|
|
21
|
+
* kann Hintergrundprozesse hinterlassen. Der Wecker löst unabhängig davon auf, damit dieser Aufruf
|
|
22
|
+
* unter keinen Umständen länger als `ZEITGRENZE_MS` offen bleibt. `unref`, damit ein laufender
|
|
23
|
+
* Wecker das Beenden des Prozesses nicht aufhält.
|
|
24
|
+
*/
|
|
25
|
+
export const shellAusfuehrer = (shell, args) => new Promise((aufloesen) => {
|
|
26
|
+
let erledigt = false;
|
|
27
|
+
const beenden = (ergebnis) => {
|
|
28
|
+
if (erledigt)
|
|
29
|
+
return;
|
|
30
|
+
erledigt = true;
|
|
31
|
+
clearTimeout(wecker);
|
|
32
|
+
aufloesen(ergebnis);
|
|
33
|
+
};
|
|
34
|
+
const kind = spawn(shell, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
35
|
+
const wecker = setTimeout(() => {
|
|
36
|
+
kind.kill('SIGKILL');
|
|
37
|
+
beenden({ stdout: '', code: 1 });
|
|
38
|
+
}, ZEITGRENZE_MS);
|
|
39
|
+
wecker.unref();
|
|
40
|
+
let stdout = '';
|
|
41
|
+
kind.stdout.on('data', (d) => (stdout += d));
|
|
42
|
+
kind.on('error', () => beenden({ stdout: '', code: 1 }));
|
|
43
|
+
kind.on('close', (code) => beenden({ stdout, code: code ?? 1 }));
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Liefert den PATH einer interaktiven Login-Shell des Nutzers, oder `undefined` bei Fehler,
|
|
47
|
+
* Zeitüberschreitung oder unplausibler Ausgabe. Shell aus `process.env.SHELL`, Fallback `/bin/zsh`
|
|
48
|
+
* (macOS-Default).
|
|
49
|
+
*
|
|
50
|
+
* `-i` ist nötig, nicht bequem: zsh liest `~/.zshrc` nur interaktiv, `-l` allein nur
|
|
51
|
+
* `~/.zshenv`/`~/.zprofile`/`~/.zlogin`. Wer wie auf picasso gar kein `~/.zprofile` hat und alle
|
|
52
|
+
* PATH-Erweiterungen in `~/.zshrc` stehen hat, bekäme mit `-l` allein nur das Ergebnis von
|
|
53
|
+
* `path_helper` zurück — also genau nicht `~/.local/bin` mit der claude-CLI, um die es hier geht
|
|
54
|
+
* (gemessen 2026-09-05: `-l` ohne `-i` liefert dort kein `~/.local/bin`, `-i -l` liefert es).
|
|
55
|
+
* Das Hänge-Risiko einer interaktiven Shell fängt der Ausführer ab: stdin zu und harte Zeitgrenze.
|
|
56
|
+
*
|
|
57
|
+
* Der Marker trennt den PATH von allem, was eine gesprächige `~/.zshrc` vorher auf stdout schreibt
|
|
58
|
+
* (Begrüßung, Versionshinweis, Prompt-Framework) — ohne ihn landete das Geschwätz im ersten
|
|
59
|
+
* PATH-Eintrag. Alles vor dem letzten Marker wird verworfen; fehlt er, gilt die Ausgabe als
|
|
60
|
+
* unbrauchbar.
|
|
61
|
+
*/
|
|
62
|
+
export const MARKER = '__hmgr_path__';
|
|
63
|
+
export async function pathAusLoginShellErmitteln(ausfuehrer = shellAusfuehrer) {
|
|
64
|
+
const shell = process.env.SHELL && process.env.SHELL.trim() !== '' ? process.env.SHELL : '/bin/zsh';
|
|
65
|
+
const { stdout, code } = await ausfuehrer(shell, ['-i', '-l', '-c', `printf %s%s '${MARKER}' "$PATH"`]);
|
|
66
|
+
if (code !== 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const ab = stdout.lastIndexOf(MARKER);
|
|
69
|
+
if (ab === -1)
|
|
70
|
+
return undefined;
|
|
71
|
+
const pfad = stdout.slice(ab + MARKER.length).trim();
|
|
72
|
+
if (pfad === '' || !pfad.includes('/'))
|
|
73
|
+
return undefined;
|
|
74
|
+
return pfad;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Führt den Login-PATH vor den bestehenden PATH: neue Einträge zuerst, danach der bisherige PATH,
|
|
78
|
+
* Duplikate raus (erstes Vorkommen zählt). So verliert die App nie einen Eintrag aus dem
|
|
79
|
+
* bisherigen PATH, gewinnt aber die zusätzlichen Pfade der Login-Shell. Kein Voll-Env-Import
|
|
80
|
+
* anderer Variablen — nur der PATH wird übernommen, um Überraschungen zu vermeiden.
|
|
81
|
+
*
|
|
82
|
+
* Getrennt wird an `path.delimiter`, nicht fest an ':' (E67): unter Windows ist der Trenner ';',
|
|
83
|
+
* und ein hart verdrahteter Doppelpunkt zerlegte dort `C:\Windows;C:\Git\cmd` in „C", „\Windows;C"
|
|
84
|
+
* und „\Git\cmd" — der PATH wäre zerstört, sobald ein Laufwerksbuchstabe doppelt vorkommt und die
|
|
85
|
+
* Duplikatsprüfung zuschlägt. Aufgerufen wird die Funktion unter Windows zwar nicht mehr
|
|
86
|
+
* (`loginPathUebernehmen` steigt dort vorher aus), sie soll aber für sich genommen richtig sein.
|
|
87
|
+
* Der Trenner ist ein Parameter, damit das auf jedem Rechner prüfbar ist.
|
|
88
|
+
*/
|
|
89
|
+
export function pathZusammenfuehren(bisherigerPath, loginPath, trenner = path.delimiter) {
|
|
90
|
+
const eintraege = [...loginPath.split(trenner), ...(bisherigerPath ?? '').split(trenner)].filter((eintrag) => eintrag !== '');
|
|
91
|
+
const gesehen = new Set();
|
|
92
|
+
const ergebnis = [];
|
|
93
|
+
for (const eintrag of eintraege) {
|
|
94
|
+
if (gesehen.has(eintrag))
|
|
95
|
+
continue;
|
|
96
|
+
gesehen.add(eintrag);
|
|
97
|
+
ergebnis.push(eintrag);
|
|
98
|
+
}
|
|
99
|
+
return ergebnis.join(trenner);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Ermittelt den Login-PATH und setzt ihn (vorangestellt) auf `process.env.PATH`. Bei Fehler,
|
|
103
|
+
* Timeout oder leerer/unplausibler Ausgabe bleibt `process.env.PATH` unverändert — dieser Aufruf
|
|
104
|
+
* darf die App nie am Start hindern.
|
|
105
|
+
*
|
|
106
|
+
* Unter Windows passiert nichts (E67). Zwei Gründe, jeder für sich ausreichend: dort gibt es keine
|
|
107
|
+
* Login-Shell im POSIX-Sinn, `$SHELL` ist normalerweise gar nicht gesetzt und der Aufruf liefe
|
|
108
|
+
* gegen das nicht vorhandene `/bin/zsh` ins Leere; wo `$SHELL` doch gesetzt ist (Git-Bash- und
|
|
109
|
+
* MSYS-Umgebungen setzen es), käme ein POSIX-PATH mit ':' als Trenner zurück und würde dem echten
|
|
110
|
+
* Windows-PATH vorangestellt — kein Gewinn, aber ein Risiko. Die Plattform ist ein Parameter, damit
|
|
111
|
+
* der Ausstieg auf jedem Rechner prüfbar ist.
|
|
112
|
+
*/
|
|
113
|
+
export async function loginPathUebernehmen(ausfuehrer = shellAusfuehrer, plattform = process.platform) {
|
|
114
|
+
if (plattform === 'win32')
|
|
115
|
+
return;
|
|
116
|
+
const loginPath = await pathAusLoginShellErmitteln(ausfuehrer);
|
|
117
|
+
if (loginPath === undefined)
|
|
118
|
+
return;
|
|
119
|
+
process.env.PATH = pathZusammenfuehren(process.env.PATH, loginPath);
|
|
120
|
+
}
|
package/dist/gui/main.js
CHANGED
|
@@ -2,19 +2,41 @@
|
|
|
2
2
|
// registriert IPC-Handler für den Renderer und implementiert den Rückfrage-Port als Dialog im
|
|
3
3
|
// Fenster. Kein zweiter Prozess: der Kern läuft direkt im Main-Prozess, die GUI ist ein zweiter
|
|
4
4
|
// Kompositions-Einstieg neben der CLI (ARCHITEKTUR.md „Eingang").
|
|
5
|
-
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
|
5
|
+
import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import os from 'node:os';
|
|
8
7
|
import { randomUUID } from 'node:crypto';
|
|
9
8
|
import { stat } from 'node:fs/promises';
|
|
10
9
|
import { fileURLToPath } from 'node:url';
|
|
11
10
|
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
12
11
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
13
12
|
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
13
|
+
import { kannProfile } from "../ports/harness.js";
|
|
14
14
|
import { iconFuerQuelle } from "./icons.js";
|
|
15
15
|
import { entscheideOeffnen } from "./oeffnen.js";
|
|
16
|
+
import { mcpUebernahmeSerialisieren, mcpUebersichtSerialisieren, mcpWirkungSerialisieren } from "./mcp-ansicht.js";
|
|
17
|
+
import { beobachten } from "./beobachter.js";
|
|
18
|
+
import { loginPathUebernehmen } from "./login-pfad.js";
|
|
16
19
|
const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
|
|
17
20
|
const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
|
|
21
|
+
/**
|
|
22
|
+
* Nur eine hmgr-Instanz je Nutzer (E67). Grund: zwei laufende Fenster schrieben nacheinander auf
|
|
23
|
+
* dieselbe `state.json` und überschrieben sich gegenseitig — der Zustand ist einer, nicht einer je
|
|
24
|
+
* Fenster. Die zweite Instanz bekommt den Lock nicht und beendet sich; die erste holt in
|
|
25
|
+
* `second-instance` ihr Fenster nach vorn, damit der zweite Start nicht wirkungslos aussieht.
|
|
26
|
+
* Gilt auf allen Plattformen: unter macOS ist der Doppelstart selten (das Dock startet nicht neu),
|
|
27
|
+
* unter Windows ist ein zweiter Klick im Startmenü der Normalfall.
|
|
28
|
+
*
|
|
29
|
+
* Der Lock wird ganz früh geholt, noch vor `bauen()` und der PATH-Ermittlung: eine Instanz, die
|
|
30
|
+
* ohnehin gleich wieder geht, soll nichts mehr anfassen und keine Shell mehr starten.
|
|
31
|
+
*
|
|
32
|
+
* Im Smoke-Test ist er aus. Er hängt am Electron-userData-Verzeichnis, und das leitet macOS nicht
|
|
33
|
+
* über das gefälschte HOME um (gemessen, siehe E67) — eine offene hmgr-GUI auf dem Rechner würde
|
|
34
|
+
* den Smoke-Lauf sonst stumm abwürgen. Dieselbe Zurückhaltung wie bei Watcher und
|
|
35
|
+
* Auto-Aktualisieren: was einen automatisierten Lauf von außen stören kann, bleibt im Smoke aus.
|
|
36
|
+
*/
|
|
37
|
+
const einzelinstanz = smoketest || app.requestSingleInstanceLock();
|
|
38
|
+
if (!einzelinstanz)
|
|
39
|
+
app.quit();
|
|
18
40
|
const umgebung = umgebungLesen();
|
|
19
41
|
/** Cache der Quellen-Icons (E60), unterhalb von HMGR_HOME wie der Zustand: Tests/Smoke fassen so
|
|
20
42
|
nie ein echtes Verzeichnis an. */
|
|
@@ -23,10 +45,16 @@ const iconCacheVerzeichnis = path.join(umgebung.hmgrHome, 'icon-cache');
|
|
|
23
45
|
const SECHS_STUNDEN_MS = 6 * 60 * 60 * 1000;
|
|
24
46
|
/**
|
|
25
47
|
* Fake der claude-CLI für den Smoke-Test (E40 R4), nach dem Muster von `fakeClaude` in
|
|
26
|
-
* tests/helfer.ts
|
|
27
|
-
*
|
|
48
|
+
* tests/helfer.ts. Kein Kindprozess, keine echte CLI: ein automatisierter Lauf fasst die echte
|
|
49
|
+
* claude-CLI formal nie an, auch wenn sie auf dem PATH liegt.
|
|
50
|
+
*
|
|
51
|
+
* `--version` läuft durch, damit Claude Code im Smoke-Test seine Fähigkeiten bekommt (E61: die
|
|
52
|
+
* MCP-Fähigkeit hängt an der vorhandenen CLI). Sonst blieben die Claude-Code-Zeilen der MCP-Ansicht
|
|
53
|
+
* ungetestet. Jeder andere Aufruf scheitert mit Code 1: die lesenden Pfade tragen das (Plugin-Liste
|
|
54
|
+
* leer, `eingehaengte` liest ohnehin nur ~/.claude.json), und die schreibenden bleiben so
|
|
55
|
+
* verschlossen — der Smoke-Test hängt nichts ein und aus, er öffnet die Dialoge nur.
|
|
28
56
|
*/
|
|
29
|
-
const smoketestClaudeAusfuehrer = async () =>
|
|
57
|
+
const smoketestClaudeAusfuehrer = async (args) => args[0] === '--version' ? { stdout: 'claude 0.0.0-smoke\n', code: 0 } : { stdout: '', code: 1 };
|
|
30
58
|
let hauptfenster = null;
|
|
31
59
|
/**
|
|
32
60
|
* Rückfrage-Port als Dialog im Renderer (E28, E34): der Main-Prozess schickt die Frage per
|
|
@@ -63,12 +91,34 @@ class GuiRueckfrage {
|
|
|
63
91
|
return typeof antwort === 'number' ? antwort : undefined;
|
|
64
92
|
}
|
|
65
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* PATH aus der Login-Shell (F47=O2, E63): aus dem Finder gestartet bekommt die App nur den
|
|
96
|
+
* macOS-Minimal-PATH, ohne z. B. ~/.local/bin mit der claude-CLI. Die Ermittlung läuft ab hier
|
|
97
|
+
* parallel zum Electron-Start; gewartet wird zentral in `auffangen`, und damit vor jedem
|
|
98
|
+
* Adapter-Aufruf, denn jeder IPC-Handler geht durch `auffangen` und Spawns gibt es nur dort
|
|
99
|
+
* (claude-code.ts, git.ts — Watcher, Fokus-Reload und Auto-Aktualisieren stoßen den Renderer an,
|
|
100
|
+
* der wieder über IPC hereinkommt).
|
|
101
|
+
*
|
|
102
|
+
* Bewusst kein Top-Level-`await`: im ESM-Main-Entry läuft vor `ready` nur, was die Imports als
|
|
103
|
+
* Nebeneffekt tun; ein Await dort verschiebt die Registrierung aller App-Handler und verzögert
|
|
104
|
+
* das Fenster um die Laufzeit der Shell (~2 s). So erscheint das Fenster sofort, und höchstens
|
|
105
|
+
* der erste Datenabruf wartet. Im Smoke-Test unterbleibt die Ermittlung ganz (nie eine echte
|
|
106
|
+
* Shell, Muster: Watcher/Auto-Aktualisieren sind im Smoke auch aus). Der `catch` ist die letzte
|
|
107
|
+
* Leine: dieser Aufruf darf die App unter keinen Umständen stören.
|
|
108
|
+
*
|
|
109
|
+
* Unter Windows steigt `loginPathUebernehmen` selbst sofort aus (E67): dort gibt es keine
|
|
110
|
+
* Login-Shell im POSIX-Sinn und keine PATH-Lücke zu füllen.
|
|
111
|
+
*/
|
|
112
|
+
const pathBereit = smoketest || !einzelinstanz
|
|
113
|
+
? Promise.resolve()
|
|
114
|
+
: loginPathUebernehmen().catch(() => undefined);
|
|
66
115
|
const rueckfrage = new GuiRueckfrage();
|
|
67
|
-
const { anwendungsfaelle } = bauen(umgebung, {
|
|
116
|
+
const { anwendungsfaelle, ports } = bauen(umgebung, {
|
|
68
117
|
rueckfrage,
|
|
69
118
|
...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
|
|
70
119
|
});
|
|
71
120
|
async function auffangen(fn) {
|
|
121
|
+
await pathBereit; // erst der Login-PATH (F47=O2), dann der Adapter — sonst findet er die CLI nicht
|
|
72
122
|
try {
|
|
73
123
|
return { ok: true, daten: await fn() };
|
|
74
124
|
}
|
|
@@ -97,8 +147,13 @@ function zielAusIpc(harness, ort) {
|
|
|
97
147
|
}
|
|
98
148
|
function registriereIpc() {
|
|
99
149
|
ipcMain.handle('hmgr:version', async () => auffangen(async () => app.getVersion()));
|
|
100
|
-
// Für pfadKurz im Renderer (Anzeige-Kürzung auf ~): nur lesend, nie in einen anderen Aufruf
|
|
101
|
-
|
|
150
|
+
// Für pfadKurz im Renderer (Anzeige-Kürzung auf ~): nur lesend, nie in einen anderen Aufruf
|
|
151
|
+
// zurückgespeist. Dasselbe Home wie der Kern (`umgebungLesen`: `HOME` vor `os.homedir()`, E68) —
|
|
152
|
+
// sonst kürzt die Anzeige dort nicht, wo HOME abweichend gesetzt ist (MSYS/Git-Bash).
|
|
153
|
+
ipcMain.handle('hmgr:home', async () => auffangen(async () => umgebung.home));
|
|
154
|
+
// Ebenfalls nur für die Anzeige (E68): der Renderer leitet daraus den Pfadtrenner ab, mit dem er
|
|
155
|
+
// eigene Anzeigepfade zusammensetzt („~/.claude.json" bzw. „~\.claude.json").
|
|
156
|
+
ipcMain.handle('hmgr:plattform', async () => auffangen(async () => process.platform));
|
|
102
157
|
ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand()).map(zielZustandSerialisieren)));
|
|
103
158
|
ipcMain.handle('hmgr:marketplaces', async () => auffangen(async () => (await anwendungsfaelle.marketplaces()).map((m) => ({
|
|
104
159
|
quelleText: quelleAlsText(m.quelle),
|
|
@@ -230,7 +285,7 @@ function registriereIpc() {
|
|
|
230
285
|
// Quelle öffnen (E60): die Protokoll-Weiche steht in oeffnen.ts (rein, testbar), hier nur die
|
|
231
286
|
// Wirkung. `shell.openExternal` bekommt ausschließlich http(s)-URLs, `shell.openPath` nur einen
|
|
232
287
|
// Ordner, den es wirklich gibt — nichts anderes wird zugelassen. Alles, was es gibt, aber kein
|
|
233
|
-
// Ordner ist, wird im
|
|
288
|
+
// Ordner ist, wird im Dateimanager nur gezeigt, nicht geöffnet: `openPath` startet sonst eine Datei
|
|
234
289
|
// mit ihrem Standardprogramm, und ein Pfad aus dem Renderer soll nichts starten können.
|
|
235
290
|
ipcMain.handle('hmgr:quelleOeffnen', async (_event, quelleText) => auffangen(async () => {
|
|
236
291
|
const entscheidung = entscheideOeffnen(quelleAusText(quelleText));
|
|
@@ -247,7 +302,83 @@ function registriereIpc() {
|
|
|
247
302
|
}
|
|
248
303
|
return entscheidung;
|
|
249
304
|
}));
|
|
305
|
+
// MCP-Ansicht (E61-Folgerunde): Whitelisting und der F39-Vermerk je Karte stehen in mcp-ansicht.ts
|
|
306
|
+
// (rein, ohne Electron, wie oeffnen.ts und icons.ts), damit sie testbar sind. Der Main-Prozess
|
|
307
|
+
// ruft nur noch auf und liefert die eigene Prozess-Umgebung für die F39-Prüfung.
|
|
308
|
+
ipcMain.handle('hmgr:mcpUebersicht', async () => auffangen(async () => mcpUebersichtSerialisieren(await anwendungsfaelle.mcpUebersicht(), process.env)));
|
|
309
|
+
// Plätze für den Einhängen-Dialog (F42): die Fähigkeit hängt am Adapter, nicht am Namen (E16),
|
|
310
|
+
// deshalb kommt die Liste unverändert aus dem Kern statt aus einer Harness-Fallunterscheidung hier.
|
|
311
|
+
ipcMain.handle('hmgr:mcpPlaetze', async () => auffangen(async () => anwendungsfaelle.mcpPlaetze()));
|
|
312
|
+
ipcMain.handle('hmgr:mcpEinhaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpEinhaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
313
|
+
ipcMain.handle('hmgr:mcpAushaengen', async (_event, args) => auffangen(async () => mcpWirkungSerialisieren(await anwendungsfaelle.mcpAushaengen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
314
|
+
// Übernehmen aus der geteilten .mcp.json (E64): die Bestätigung holt der Renderer ein, hier läuft
|
|
315
|
+
// nur die Ausführung. Der Name muss nicht im Katalog stehen.
|
|
316
|
+
ipcMain.handle('hmgr:mcpUebernehmen', async (_event, args) => auffangen(async () => mcpUebernahmeSerialisieren(await anwendungsfaelle.mcpUebernehmen({ name: args.name, ziel: zielAusIpc(args.harness, args.ort) }))));
|
|
317
|
+
}
|
|
318
|
+
/** Mindestabstand zwischen zwei Fokus-Reloads (E62): schnelles Fenster-Wechseln löst keine Lawine
|
|
319
|
+
von Anzeige-Neuladungen aus. Getrennt vom Entprellen im Datei-Watcher (das sammelt Events vor dem
|
|
320
|
+
ersten Aufruf), hier wird zusätzlich zwischen aufeinanderfolgenden Aufrufen gedrosselt (trailing:
|
|
321
|
+
ein Aufruf innerhalb der Sperrzeit wird nicht verworfen, sondern ans Ende der Sperrzeit verschoben). */
|
|
322
|
+
const FOKUS_RELOAD_MIN_ABSTAND_MS = 3000;
|
|
323
|
+
/** Sammelzeit des Datei-Watchers (E62): mehrere Events in kurzer Folge lösen einen Reload aus. */
|
|
324
|
+
const BEOBACHTER_ENTPRELLEN_MS = 400;
|
|
325
|
+
/**
|
|
326
|
+
* Pfade, die die GUI kennt und darum beobachtet (E62): hmgr-Zustand samt MCP-Katalog, die
|
|
327
|
+
* Harness-MCP-Konfigurationen (`~/.claude.json`, Hermes' `config.yaml`) und die `.env`-Dateien, die
|
|
328
|
+
* die F39-Prüfung beim MCP-Einhängen liest — bei Hermes liegt die `.env` je Ort neben der
|
|
329
|
+
* `config.yaml` (so liest sie der Adapter in `mcp.envDatei`), darum auch je Profil. Die Profile
|
|
330
|
+
* werden einmal beim Start am Harness selbst erfragt (`profile()`, ein Verzeichnis-Blick, kein
|
|
331
|
+
* ganzer Erhebungslauf); ein zur Laufzeit neu angelegtes Profil beobachtet der Watcher nicht
|
|
332
|
+
* dynamisch, das deckt der Fokus-Reload ab (E62).
|
|
333
|
+
*
|
|
334
|
+
* Nicht dabei: die geteilten `.mcp.json` der Arbeitsverzeichnisse, die die MCP-Übersicht seit dem
|
|
335
|
+
* E61-Nachtrag mitliest. Ihre Liste ist zur Laufzeit veränderlich, wie die der Hermes-Profile, und
|
|
336
|
+
* damit gilt für sie dasselbe: der Fokus-Reload deckt sie ab.
|
|
337
|
+
*/
|
|
338
|
+
async function beobachtetePfade() {
|
|
339
|
+
const hermesHome = path.join(umgebung.home, '.hermes');
|
|
340
|
+
const pfade = [
|
|
341
|
+
path.join(umgebung.hmgrHome, 'state.json'),
|
|
342
|
+
path.join(umgebung.hmgrHome, 'mcp.yaml'),
|
|
343
|
+
path.join(umgebung.home, '.claude.json'),
|
|
344
|
+
path.join(hermesHome, 'config.yaml'),
|
|
345
|
+
path.join(hermesHome, '.env'),
|
|
346
|
+
];
|
|
347
|
+
const hermes = ports.harnesses.find((h) => h.name === 'hermes');
|
|
348
|
+
if (hermes !== undefined && kannProfile(hermes))
|
|
349
|
+
for (const name of await hermes.profile().catch(() => [])) {
|
|
350
|
+
const profilHome = path.join(hermesHome, 'profiles', name);
|
|
351
|
+
pfade.push(path.join(profilHome, 'config.yaml'), path.join(profilHome, '.env'));
|
|
352
|
+
}
|
|
353
|
+
return pfade;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Stößt beim Renderer ein stilles Neuladen der Anzeige an (E62), nie einen Aktualisieren-Lauf mit
|
|
357
|
+
* Netzzugriff. Gedrosselt (trailing throttle): unmittelbar, wenn seit dem letzten Anstoß genug Zeit
|
|
358
|
+
* verging, sonst genau ein nachgeholter Anstoß am Ende der Sperrzeit.
|
|
359
|
+
*/
|
|
360
|
+
function neuLadenAnstossenFabrik(minAbstandMs) {
|
|
361
|
+
let letzter = 0;
|
|
362
|
+
let timer;
|
|
363
|
+
return function anstossen() {
|
|
364
|
+
const jetzt = Date.now();
|
|
365
|
+
const seitLetztem = jetzt - letzter;
|
|
366
|
+
if (seitLetztem >= minAbstandMs) {
|
|
367
|
+
letzter = jetzt;
|
|
368
|
+
hauptfenster?.webContents.send('hmgr:neuLadenAnstossen');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (timer !== undefined)
|
|
372
|
+
return; // schon ein Anstoß fürs Ende der Sperrzeit vorgemerkt
|
|
373
|
+
timer = setTimeout(() => {
|
|
374
|
+
timer = undefined;
|
|
375
|
+
letzter = Date.now();
|
|
376
|
+
hauptfenster?.webContents.send('hmgr:neuLadenAnstossen');
|
|
377
|
+
}, minAbstandMs - seitLetztem);
|
|
378
|
+
};
|
|
250
379
|
}
|
|
380
|
+
const neuLadenAnstossen = neuLadenAnstossenFabrik(FOKUS_RELOAD_MIN_ABSTAND_MS);
|
|
381
|
+
let beobachter;
|
|
251
382
|
function fensterOeffnen() {
|
|
252
383
|
hauptfenster = new BrowserWindow({
|
|
253
384
|
width: 1280,
|
|
@@ -368,6 +499,9 @@ function fensterOeffnen() {
|
|
|
368
499
|
// E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
|
|
369
500
|
// Zahnrad statt Sonne am Einstellungen-Knopf.
|
|
370
501
|
var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
|
|
502
|
+
// E55-Marken auch in der Ziel-Tabelle, unter der Beschreibung (2026-09-05).
|
|
503
|
+
var zielMarken = document.querySelector('#zielliste .linkliste .wasfeld .marken')?.textContent
|
|
504
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
371
505
|
var zielManuellVorhanden = (document.getElementById('zielliste')?.textContent ?? '').includes('manuell');
|
|
372
506
|
var zahnradVorhanden = (document.querySelector('#einstellungen svg')?.innerHTML ?? '').includes('6.47 3.56');
|
|
373
507
|
// Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
|
|
@@ -396,6 +530,137 @@ function fensterOeffnen() {
|
|
|
396
530
|
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
397
531
|
document.getElementById('d-skills')?.close();
|
|
398
532
|
}
|
|
533
|
+
// MCP-Ansicht (E61-Folgerunde, F46: abgespeckt zum reinen Katalog): Navigationsreihenfolge
|
|
534
|
+
// Ziele, MCP, Plugins, Skills, Karten mit Name/Beschreibung/Homepage-Link, F39-Warnzeile,
|
|
535
|
+
// Einhängen-Dialog öffnen/abbrechen ohne IPC-Schreibaktion. Orte-Liste und Fremdkasten
|
|
536
|
+
// gehören seit F46 nicht mehr in diese Ansicht (die Prüfung darauf ist unten invertiert),
|
|
537
|
+
// sie leben nur noch am MCP-Abschnitt der Ziel-Karten (weiter unten unverändert geprüft).
|
|
538
|
+
var navReihenfolge = Array.from(document.querySelectorAll('.nav [data-ansicht]'))
|
|
539
|
+
.map((b) => b.dataset.ansicht).join(',');
|
|
540
|
+
document.querySelector('[data-ansicht="mcp"]')?.click();
|
|
541
|
+
var mcpKartenText = document.getElementById('mcpliste')?.textContent
|
|
542
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
543
|
+
var mcpHeimLinkVorhanden = document.querySelector('#mcpliste .mcpkopf .qlink') !== null;
|
|
544
|
+
var mcpWarnzeileVorhanden = document.querySelector('#mcpliste .mcp .ruhezeile') !== null;
|
|
545
|
+
// F46: die Orte-Liste je Karte und der Fremdkasten der Ansicht sind ganz weg, nicht nur
|
|
546
|
+
// leer — beide Selektoren dürfen im DOM überhaupt nicht mehr auftauchen.
|
|
547
|
+
var mcpOrteVorhanden = document.querySelector('#mcpliste .mcporte') !== null;
|
|
548
|
+
var mcpFremdkastenVorhanden = document.getElementById('mcpfremd') !== null;
|
|
549
|
+
var mcpFehler = null;
|
|
550
|
+
var dMcpOffenNachOeffnen = false;
|
|
551
|
+
var dMcpOffenNachAbbrechen = true;
|
|
552
|
+
// Die Wahlzeilen des Einhängen-Dialogs (F42): sie kommen aus der Adapter-Fähigkeit,
|
|
553
|
+
// hier steht, welche Orte zur Wahl stehen und wohin der Eintrag jeweils ginge. Zwei
|
|
554
|
+
// Karten, weil die Zeilen verschieden aussehen: „mail" hängt an beiden Home-Orten
|
|
555
|
+
// schon (gesperrte Zeile), „teamwork" nirgends (die Notizen je Ort).
|
|
556
|
+
var mcpWahlText = null;
|
|
557
|
+
var mcpWahlTextFrei = null;
|
|
558
|
+
try {
|
|
559
|
+
var mcpEinKnoepfe = Array.from(document.querySelectorAll('#mcpliste .mcpkopf button'))
|
|
560
|
+
.filter((b) => (b.textContent || '').indexOf('Einhängen') === 0);
|
|
561
|
+
var mcpWahlOeffnen = async (knopf) => {
|
|
562
|
+
knopf?.click();
|
|
563
|
+
// window.hmgr.mcpPlaetze() ist asynchron: kurz nachfassen, bis die Wahlzeilen stehen.
|
|
564
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
565
|
+
return document.getElementById('dm-orte')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
566
|
+
};
|
|
567
|
+
mcpWahlText = await mcpWahlOeffnen(mcpEinKnoepfe[0]);
|
|
568
|
+
dMcpOffenNachOeffnen = document.getElementById('d-mcp')?.open === true;
|
|
569
|
+
document.querySelector('#d-mcp button[value="abbruch"]')?.click();
|
|
570
|
+
dMcpOffenNachAbbrechen = document.getElementById('d-mcp')?.open === true;
|
|
571
|
+
mcpWahlTextFrei = await mcpWahlOeffnen(mcpEinKnoepfe[1]);
|
|
572
|
+
document.querySelector('#d-mcp button[value="abbruch"]')?.click();
|
|
573
|
+
} catch (fehler) {
|
|
574
|
+
mcpFehler = String(fehler);
|
|
575
|
+
}
|
|
576
|
+
// Aushängen-Rückfrage (F46: nur noch über den Entfernen-Knopf am MCP-Abschnitt einer
|
|
577
|
+
// Ziel-Karte auslösbar, die MCP-Ansicht selbst hat dafür keinen Knopf mehr).
|
|
578
|
+
var mcpAusFehler = null;
|
|
579
|
+
var dMcpAusOffenNachOeffnen = false;
|
|
580
|
+
var dMcpAusOffenNachAbbrechen = true;
|
|
581
|
+
// Seit E64 stehen im MCP-Abschnitt zwei Knopfarten nebeneinander (Entfernen und
|
|
582
|
+
// Übernehmen), deshalb wird hier nicht mehr der erste Knopf genommen, sondern der
|
|
583
|
+
// Entfernen-Knopf an seinem aria-label erkannt.
|
|
584
|
+
var mcpZeilenKnoepfe = Array.from(document.querySelectorAll('#zielliste .mcpkartezeilen button'));
|
|
585
|
+
try {
|
|
586
|
+
var mcpAusKnopf = mcpZeilenKnoepfe.find(function (b) {
|
|
587
|
+
return (b.getAttribute('aria-label') || '').indexOf('aushängen') > -1;
|
|
588
|
+
});
|
|
589
|
+
mcpAusKnopf?.click();
|
|
590
|
+
dMcpAusOffenNachOeffnen = document.getElementById('d-mcpaus')?.open === true;
|
|
591
|
+
document.querySelector('#d-mcpaus button[value="nein"]')?.click();
|
|
592
|
+
dMcpAusOffenNachAbbrechen = document.getElementById('d-mcpaus')?.open === true;
|
|
593
|
+
} catch (fehler) {
|
|
594
|
+
mcpAusFehler = String(fehler);
|
|
595
|
+
}
|
|
596
|
+
// Übernehmen aus der geteilten .mcp.json (E64): Knopf an einer geteilten Zeile,
|
|
597
|
+
// Rückfrage öffnen und über „Nein" wieder schließen — ohne IPC-Schreibaktion, wie bei
|
|
598
|
+
// den anderen MCP-Dialogen. Der Text der Rückfrage läuft mit, er ist die Warnung an
|
|
599
|
+
// den Nutzer und darf nicht stillschweigend verschwinden.
|
|
600
|
+
var mcpUebFehler = null;
|
|
601
|
+
var dMcpUebOffenNachOeffnen = false;
|
|
602
|
+
var dMcpUebOffenNachAbbrechen = true;
|
|
603
|
+
var mcpUebText = null;
|
|
604
|
+
try {
|
|
605
|
+
var mcpUebKnopf = mcpZeilenKnoepfe.find(function (b) {
|
|
606
|
+
return (b.textContent || '').indexOf('Übernehmen') === 0;
|
|
607
|
+
});
|
|
608
|
+
mcpUebKnopf?.click();
|
|
609
|
+
dMcpUebOffenNachOeffnen = document.getElementById('d-mcpueb')?.open === true;
|
|
610
|
+
mcpUebText = document.getElementById('dmu-text')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
611
|
+
document.querySelector('#d-mcpueb button[value="nein"]')?.click();
|
|
612
|
+
dMcpUebOffenNachAbbrechen = document.getElementById('d-mcpueb')?.open === true;
|
|
613
|
+
} catch (fehler) {
|
|
614
|
+
mcpUebFehler = String(fehler);
|
|
615
|
+
}
|
|
616
|
+
// MCP an der Ziel-Karte (E61-GUI-Nachtrag), Gegenrichtung zur MCP-Ansicht: Abschnitt und
|
|
617
|
+
// Knopf an der Hermes-Home-Karte (Katalog-Zeile mit Entfernen, Fremd-Zeile ohne Knopf)
|
|
618
|
+
// und an der kunde-x-Verzeichnis-Karte (zwei Zeilen aus der geteilten .mcp.json, seit
|
|
619
|
+
// E64 beide mit Übernehmen-Knopf), dazu die Knopf-Sichtbarkeit: an der agents-Karte
|
|
620
|
+
// darf weder Abschnitt noch Knopf stehen, agents implementiert die MCP-Fähigkeit
|
|
621
|
+
// nicht (E16).
|
|
622
|
+
var zielKarten = Array.from(document.querySelectorAll('#zielliste .ziel'));
|
|
623
|
+
var karteMitKopftext = function (t) {
|
|
624
|
+
return zielKarten.find(function (k) {
|
|
625
|
+
return (k.querySelector('.zielkopf')?.textContent || '').indexOf(t) > -1;
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
var mcpHinzuKnopfAn = function (karte) {
|
|
629
|
+
return karte
|
|
630
|
+
? Array.from(karte.querySelectorAll('button')).find((b) => (b.textContent || '').indexOf('MCP hinzufügen') === 0)
|
|
631
|
+
: undefined;
|
|
632
|
+
};
|
|
633
|
+
var hermesKarte = karteMitKopftext('Hermes · Home');
|
|
634
|
+
var hermesMcpText = hermesKarte?.querySelector('.mcpkarteabschnitt')?.textContent
|
|
635
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
636
|
+
var hermesMcpZeilenKnopfZahl = hermesKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
|
|
637
|
+
var hermesMcpKnopfVorhanden = mcpHinzuKnopfAn(hermesKarte) !== undefined;
|
|
638
|
+
var kundeXKarte = karteMitKopftext('kunde-x');
|
|
639
|
+
var kundeXMcpText = kundeXKarte?.querySelector('.mcpkarteabschnitt')?.textContent
|
|
640
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
641
|
+
var kundeXMcpZeilenKnopfZahl = kundeXKarte?.querySelectorAll('.mcpkartezeilen button').length ?? 0;
|
|
642
|
+
var kundeXMcpKnopfVorhanden = mcpHinzuKnopfAn(kundeXKarte) !== undefined;
|
|
643
|
+
var agentsKarte = karteMitKopftext('Agents · Home');
|
|
644
|
+
// Ohne diese Zusicherung wären die beiden Abwesenheits-Prüfungen darunter leer erfüllt,
|
|
645
|
+
// sobald die Karte selbst aus der Liste fiele: „kein Abschnitt" hieße dann nur „keine Karte".
|
|
646
|
+
var agentsKarteVorhanden = agentsKarte !== undefined;
|
|
647
|
+
var agentsMcpAbschnittVorhanden = (agentsKarte?.querySelector('.mcpkarteabschnitt') ?? null) !== null;
|
|
648
|
+
var agentsMcpKnopfVorhanden = mcpHinzuKnopfAn(agentsKarte) !== undefined;
|
|
649
|
+
// Dialog „MCP hinzufügen" (E47-Muster): öffnen an der Hermes-Karte und über Abbrechen
|
|
650
|
+
// wieder schließen, ohne IPC-Schreibaktion (mcpEinhaengen liefe erst nach „Einhängen").
|
|
651
|
+
var dmhFehler = null;
|
|
652
|
+
var dMcpHinzuOffenNachOeffnen = false;
|
|
653
|
+
var dMcpHinzuOffenNachAbbrechen = true;
|
|
654
|
+
var dmhListeText = null;
|
|
655
|
+
try {
|
|
656
|
+
mcpHinzuKnopfAn(hermesKarte)?.click();
|
|
657
|
+
dMcpHinzuOffenNachOeffnen = document.getElementById('d-mcp-hinzufuegen')?.open === true;
|
|
658
|
+
dmhListeText = document.getElementById('dmh-liste')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
659
|
+
document.querySelector('#d-mcp-hinzufuegen button[value="abbruch"]')?.click();
|
|
660
|
+
dMcpHinzuOffenNachAbbrechen = document.getElementById('d-mcp-hinzufuegen')?.open === true;
|
|
661
|
+
} catch (fehler) {
|
|
662
|
+
dmhFehler = String(fehler);
|
|
663
|
+
}
|
|
399
664
|
// Schriften aus fonts/ (Zusatzpunkt 6): erzwingt das Laden und belegt es danach.
|
|
400
665
|
await Promise.all([
|
|
401
666
|
document.fonts.load('400 16px Inter'),
|
|
@@ -409,6 +674,7 @@ function fensterOeffnen() {
|
|
|
409
674
|
einstellungenOffen,
|
|
410
675
|
harnessSchalter,
|
|
411
676
|
zielBeschreibung,
|
|
677
|
+
zielMarken,
|
|
412
678
|
zielManuellVorhanden,
|
|
413
679
|
zahnradVorhanden,
|
|
414
680
|
katalogBeschreibung,
|
|
@@ -443,6 +709,37 @@ function fensterOeffnen() {
|
|
|
443
709
|
dSkillordnerOffenNachOeffnen,
|
|
444
710
|
dSkillordnerOffenNachAbbrechen,
|
|
445
711
|
skillOrdnerFehler,
|
|
712
|
+
navReihenfolge,
|
|
713
|
+
mcpKartenText,
|
|
714
|
+
mcpHeimLinkVorhanden,
|
|
715
|
+
mcpWarnzeileVorhanden,
|
|
716
|
+
mcpOrteVorhanden,
|
|
717
|
+
mcpFremdkastenVorhanden,
|
|
718
|
+
dMcpOffenNachOeffnen,
|
|
719
|
+
dMcpOffenNachAbbrechen,
|
|
720
|
+
mcpWahlText,
|
|
721
|
+
mcpWahlTextFrei,
|
|
722
|
+
mcpFehler,
|
|
723
|
+
dMcpAusOffenNachOeffnen,
|
|
724
|
+
dMcpAusOffenNachAbbrechen,
|
|
725
|
+
mcpAusFehler,
|
|
726
|
+
dMcpUebOffenNachOeffnen,
|
|
727
|
+
dMcpUebOffenNachAbbrechen,
|
|
728
|
+
mcpUebText,
|
|
729
|
+
mcpUebFehler,
|
|
730
|
+
hermesMcpText,
|
|
731
|
+
hermesMcpZeilenKnopfZahl,
|
|
732
|
+
hermesMcpKnopfVorhanden,
|
|
733
|
+
kundeXMcpText,
|
|
734
|
+
kundeXMcpZeilenKnopfZahl,
|
|
735
|
+
kundeXMcpKnopfVorhanden,
|
|
736
|
+
agentsKarteVorhanden,
|
|
737
|
+
agentsMcpAbschnittVorhanden,
|
|
738
|
+
agentsMcpKnopfVorhanden,
|
|
739
|
+
dMcpHinzuOffenNachOeffnen,
|
|
740
|
+
dMcpHinzuOffenNachAbbrechen,
|
|
741
|
+
dmhListeText,
|
|
742
|
+
dmhFehler,
|
|
446
743
|
});
|
|
447
744
|
})()`)
|
|
448
745
|
.then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
|
|
@@ -461,20 +758,65 @@ function fensterOeffnen() {
|
|
|
461
758
|
const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
|
|
462
759
|
fenster.on('closed', () => clearInterval(intervall));
|
|
463
760
|
}
|
|
761
|
+
// Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
|
|
762
|
+
// Beide stoßen nur ein stilles Neuladen der Anzeige an, nie einen Aktualisieren-Lauf mit
|
|
763
|
+
// Netzzugriff. Im Smoke-Test keins von beiden, dieselbe Zurückhaltung wie beim automatischen
|
|
764
|
+
// Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
|
|
765
|
+
if (!smoketest) {
|
|
766
|
+
const fenster = hauptfenster;
|
|
767
|
+
fenster.on('focus', () => neuLadenAnstossen());
|
|
768
|
+
void beobachtetePfade().then((pfade) => {
|
|
769
|
+
if (fenster.isDestroyed())
|
|
770
|
+
return; // Fenster schon zu, während die Pfade ermittelt wurden
|
|
771
|
+
beobachter = beobachten(pfade, () => neuLadenAnstossen(), { entprellenMs: BEOBACHTER_ENTPRELLEN_MS });
|
|
772
|
+
});
|
|
773
|
+
fenster.on('closed', () => {
|
|
774
|
+
beobachter?.close();
|
|
775
|
+
beobachter = undefined;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
464
778
|
void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
|
|
465
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Holt das vorhandene Fenster nach vorn — die Antwort der laufenden Instanz auf einen zweiten
|
|
782
|
+
* Start (E67). Minimiert kommt es erst wieder hoch, dann bekommt es den Fokus.
|
|
783
|
+
*/
|
|
784
|
+
function fensterNachVornHolen() {
|
|
785
|
+
if (hauptfenster === null || hauptfenster.isDestroyed())
|
|
786
|
+
return;
|
|
787
|
+
if (hauptfenster.isMinimized())
|
|
788
|
+
hauptfenster.restore();
|
|
789
|
+
hauptfenster.show();
|
|
790
|
+
hauptfenster.focus();
|
|
791
|
+
}
|
|
466
792
|
registriereIpc();
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
793
|
+
if (einzelinstanz) {
|
|
794
|
+
app.on('second-instance', () => fensterNachVornHolen());
|
|
795
|
+
app.whenReady().then(() => {
|
|
796
|
+
// Im Smoke-Test kein Dock-Icon und kein Fokuswechsel: das Fenster bleibt unsichtbar (show:false
|
|
797
|
+
// oben), macOS würde aber ohne diese Zeile trotzdem ein Dock-Icon zeigen und aktivieren.
|
|
798
|
+
if (smoketest && process.platform === 'darwin')
|
|
799
|
+
app.dock?.hide();
|
|
800
|
+
// Windows-Fensterkonventionen (E67), beide nur dort:
|
|
801
|
+
// — Die Taskleiste gruppiert Fenster und Verknüpfungen über die AppUserModelID. Ohne sie steht
|
|
802
|
+
// der Prozess unter einer von Windows geratenen ID, und das angeheftete Startmenü-Symbol
|
|
803
|
+
// gehört dann nicht zum laufenden Fenster. Dieselbe ID wie die `appId` im build-Block, denn
|
|
804
|
+
// die schreibt der NSIS-Installer in die Verknüpfung.
|
|
805
|
+
// — Electron blendet unter Windows sonst sein Standardmenü ein (Datei/Bearbeiten/Ansicht/…),
|
|
806
|
+
// eine Menüleiste, die hmgr nicht bedient und die nur Platz über der Ansicht kostet. macOS
|
|
807
|
+
// bleibt unangetastet: dort ist die Menüleiste des Systems Pflicht (Beenden, Kopieren,
|
|
808
|
+
// Einfügen hängen daran).
|
|
809
|
+
if (process.platform === 'win32') {
|
|
810
|
+
app.setAppUserModelId('de.timkaufmann.hmgr');
|
|
811
|
+
Menu.setApplicationMenu(null);
|
|
812
|
+
}
|
|
813
|
+
fensterOeffnen();
|
|
814
|
+
app.on('activate', () => {
|
|
815
|
+
if (BrowserWindow.getAllWindows().length === 0)
|
|
816
|
+
fensterOeffnen();
|
|
817
|
+
});
|
|
476
818
|
});
|
|
477
|
-
}
|
|
819
|
+
}
|
|
478
820
|
app.on('window-all-closed', () => {
|
|
479
821
|
if (process.platform !== 'darwin')
|
|
480
822
|
app.quit();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// MCP-Ansicht der GUI (E61-Folgerunde): reine Umformung von Kern-Daten in IPC-taugliche, gewhiteliste
|
|
2
|
+
// Objekte, ohne Electron — testbar wie oeffnen.ts und icons.ts. Der eigentliche IPC-Handler in
|
|
3
|
+
// main.ts ruft diese Funktionen nur noch auf.
|
|
4
|
+
import { variablenVerweise, } from "../kern/anwendungsfaelle.js";
|
|
5
|
+
/** Katalog-Eintrag über IPC: die bekannten Felder, nichts ungefiltert durchgereicht (E40 R3). */
|
|
6
|
+
export function mcpEintragSerialisieren(eintrag) {
|
|
7
|
+
return {
|
|
8
|
+
name: eintrag.name,
|
|
9
|
+
beschreibung: eintrag.beschreibung,
|
|
10
|
+
...(eintrag.homepage === undefined ? {} : { homepage: eintrag.homepage }),
|
|
11
|
+
...(eintrag.notizen === undefined ? {} : { notizen: eintrag.notizen }),
|
|
12
|
+
konfig: eintrag.konfig,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* F39 an der Katalog-Karte: dieselbe Prüf-Funktion wie beim tatsächlichen Einhängen
|
|
17
|
+
* (`variablenVerweise`), hier aber nur gegen die übergebene Umgebung geprüft — ohne den
|
|
18
|
+
* ortsbezogenen `.env`-Abgleich, der ist ortsspezifisch und bleibt dem eigentlichen Einhängen
|
|
19
|
+
* vorbehalten (kommt von dort als `warnung` zurück).
|
|
20
|
+
*/
|
|
21
|
+
export function mcpFehlendeVariablen(eintrag, umgebung) {
|
|
22
|
+
return variablenVerweise(eintrag.konfig).filter((name) => !(name in umgebung) || umgebung[name] === undefined);
|
|
23
|
+
}
|
|
24
|
+
/** Übersicht über IPC (E61-Folgerunde): Katalog-Einträge whitelisted, dazu der F39-Vermerk je Karte. */
|
|
25
|
+
export function mcpUebersichtSerialisieren(uebersicht, umgebung) {
|
|
26
|
+
return {
|
|
27
|
+
katalog: uebersicht.katalog.map((k) => ({
|
|
28
|
+
eintrag: mcpEintragSerialisieren(k.eintrag),
|
|
29
|
+
eingehaengt: k.eingehaengt,
|
|
30
|
+
geteilteFundorte: k.geteilteFundorte,
|
|
31
|
+
fehlendeVariablen: mcpFehlendeVariablen(k.eintrag, umgebung),
|
|
32
|
+
})),
|
|
33
|
+
unbekannt: uebersicht.unbekannt,
|
|
34
|
+
unbekannteGeteilteFundorte: uebersicht.unbekannteGeteilteFundorte,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Ergebnis von `mcpEinhaengen`/`mcpAushaengen` über IPC, whitelisted wie der Katalog-Eintrag. */
|
|
38
|
+
export function mcpWirkungSerialisieren(ergebnis) {
|
|
39
|
+
return {
|
|
40
|
+
eintrag: mcpEintragSerialisieren(ergebnis.eintrag),
|
|
41
|
+
ziel: ergebnis.ziel,
|
|
42
|
+
...(ergebnis.hinweis === undefined ? {} : { hinweis: ergebnis.hinweis }),
|
|
43
|
+
...(ergebnis.warnung === undefined ? {} : { warnung: ergebnis.warnung }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ergebnis von `mcpUebernehmen` über IPC (E64). Kein Katalog-Eintrag darin: übernommen wird auch,
|
|
48
|
+
* was der Katalog nicht kennt, deshalb reicht der Name.
|
|
49
|
+
*/
|
|
50
|
+
export function mcpUebernahmeSerialisieren(ergebnis) {
|
|
51
|
+
return {
|
|
52
|
+
name: ergebnis.name,
|
|
53
|
+
ziel: ergebnis.ziel,
|
|
54
|
+
datei: ergebnis.datei,
|
|
55
|
+
...(ergebnis.hinweis === undefined ? {} : { hinweis: ergebnis.hinweis }),
|
|
56
|
+
};
|
|
57
|
+
}
|