hmgr 0.0.3 → 0.0.5
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/dist/adapter/checkout-pfad.js +5 -20
- package/dist/adapter/claude-code.js +21 -17
- package/dist/adapter/dateisystem.js +105 -5
- package/dist/adapter/git.js +124 -11
- package/dist/adapter/hermes.js +163 -10
- package/dist/adapter/quelle-text.js +1 -3
- package/dist/adapter/skill-md.js +94 -18
- package/dist/adapter/terminal.js +24 -1
- package/dist/adapter/zustand-json.js +4 -0
- package/dist/cli/hilfe.js +144 -27
- package/dist/cli/main.js +374 -28
- package/dist/gui/icons.js +114 -0
- package/dist/gui/main.js +481 -0
- package/dist/gui/oeffnen.js +33 -0
- package/dist/gui/preload.cjs +55 -0
- package/dist/gui/renderer/fonts/Inter-LICENSE.txt +92 -0
- package/dist/gui/renderer/fonts/InterVariable.woff2 +0 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-Medium.woff2 +0 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-OFL.txt +93 -0
- package/dist/gui/renderer/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/dist/gui/renderer/index.html +491 -0
- package/dist/gui/renderer/renderer.js +2611 -0
- package/dist/gui/renderer/styles.css +638 -0
- package/dist/kern/anwendungsfaelle.js +798 -68
- package/dist/kern/modell.js +175 -1
- package/dist/kern/quelle-ort.js +42 -0
- package/dist/ports/harness.js +13 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Quellen-Icon im Main-Prozess (E60): Besitzer-Avatar bei GitHub/Gitea, sonst Favicon, sonst
|
|
2
|
+
// generisch. Lädt nur hier (nie im Renderer, contextIsolation lässt dort ohnehin kein fetch mit
|
|
3
|
+
// Netzzugriff zu, das hier ist reine Main-Prozess-Politik), cached als data-URL auf Platte
|
|
4
|
+
// unterhalb von HMGR_HOME (Aufrufer reicht das Verzeichnis rein), damit Tests und der Smoke-Test
|
|
5
|
+
// nie ein echtes Verzeichnis anfassen. Ein Fehlschlag wird mitgecacht, aber nur für einen Tag: ein
|
|
6
|
+
// Treffer gilt für immer, ein Fehlschlag verfällt, sonst bliebe es bei generischen Symbolen für
|
|
7
|
+
// jeden, der beim ersten Start ohne Netz war. „lokal" und jede unbekannte Adresse ergeben sofort null.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { urlOrt } from "../kern/quelle-ort.js";
|
|
12
|
+
const STANDARD_TIMEOUT_MS = 5000;
|
|
13
|
+
/**
|
|
14
|
+
* Ein Fehlschlag hält einen Tag, ein Treffer hält für immer. Ohne Verfall bekäme jeder, der beim
|
|
15
|
+
* ersten Start offline war, dauerhaft nur generische Symbole — der Cache soll das Netz schonen,
|
|
16
|
+
* nicht ein einmaliges „gerade kein Netz" festschreiben.
|
|
17
|
+
*/
|
|
18
|
+
const FEHLSCHLAG_HALTBARKEIT_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
/**
|
|
20
|
+
* Obergrenze für ein Icon. Ein Avatar oder Favicon ist ein paar Kilobyte groß; alles darüber ist
|
|
21
|
+
* kein Symbol mehr und hätte als data-URL nichts im Cache und in der Ansicht verloren.
|
|
22
|
+
*/
|
|
23
|
+
const MAX_ICON_BYTES = 512 * 1024;
|
|
24
|
+
/**
|
|
25
|
+
* Kandidaten-URLs für den Besitzer-Avatar, in Versuchsreihenfolge (F35): GitHub liefert den Avatar
|
|
26
|
+
* über einen eigenen Pfad, jeder andere http(s)-Host bekommt zuerst den mutmaßlichen Avatar-Pfad
|
|
27
|
+
* versucht (das bedient auch Gitea), dann sein Favicon.
|
|
28
|
+
*/
|
|
29
|
+
export function iconKandidaten(ort) {
|
|
30
|
+
if (ort.host === 'github.com')
|
|
31
|
+
return [`https://github.com/${ort.besitzer}.png?size=40`];
|
|
32
|
+
return [`https://${ort.host}/${ort.besitzer}.png`, `https://${ort.host}/favicon.ico`];
|
|
33
|
+
}
|
|
34
|
+
function cacheDateiname(schluessel) {
|
|
35
|
+
return `${createHash('sha256').update(schluessel).digest('hex')}.txt`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Die Zeitüberschreitung umfasst auch das Lesen des Rumpfs, nicht nur den Kopf: ein Server, der die
|
|
39
|
+
* Kopfzeilen sofort schickt und dann tröpfelt, würde den Aufruf sonst unbegrenzt hängen lassen.
|
|
40
|
+
* Deshalb läuft die ganze Arbeit unter demselben Signal, der Wecker fällt erst danach.
|
|
41
|
+
*/
|
|
42
|
+
async function mitTimeout(arbeit, timeoutMs) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
return await arbeit(controller.signal);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function alsDataUrl(antwort) {
|
|
53
|
+
if (!antwort.ok)
|
|
54
|
+
return undefined;
|
|
55
|
+
const typ = antwort.headers.get('content-type') ?? '';
|
|
56
|
+
if (!typ.startsWith('image/'))
|
|
57
|
+
return undefined;
|
|
58
|
+
const angekuendigt = Number(antwort.headers.get('content-length'));
|
|
59
|
+
if (Number.isFinite(angekuendigt) && angekuendigt > MAX_ICON_BYTES)
|
|
60
|
+
return undefined;
|
|
61
|
+
const puffer = Buffer.from(await antwort.arrayBuffer());
|
|
62
|
+
if (puffer.length === 0 || puffer.length > MAX_ICON_BYTES)
|
|
63
|
+
return undefined;
|
|
64
|
+
return `data:${typ.split(';')[0]};base64,${puffer.toString('base64')}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Was im Cache steht: eine data-URL (Treffer, gilt immer), oder ein Fehlschlag (leere Datei), der
|
|
68
|
+
* nur bis zu seiner Haltbarkeit gilt. Fehlt beides, muss abgefragt werden.
|
|
69
|
+
*/
|
|
70
|
+
async function ausCache(cachePfad) {
|
|
71
|
+
const inhalt = await readFile(cachePfad, 'utf8').catch(() => undefined);
|
|
72
|
+
if (inhalt === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (inhalt.length > 0)
|
|
75
|
+
return { wert: inhalt };
|
|
76
|
+
const info = await stat(cachePfad).catch(() => undefined);
|
|
77
|
+
if (info && Date.now() - info.mtimeMs < FEHLSCHLAG_HALTBARKEIT_MS)
|
|
78
|
+
return { wert: null };
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Icon einer Quelle als data-URL, oder null (lokale Quelle, unlesbare Adresse oder jeder
|
|
83
|
+
* Fehlschlag: Zeitüberschreitung, Netzfehler, kein Bild). Das Ergebnis – auch ein Fehlschlag –
|
|
84
|
+
* landet auf Platte, ein zweiter Aufruf für denselben Host/Besitzer fragt nicht erneut ab; ein
|
|
85
|
+
* gecachter Fehlschlag verfällt nach einem Tag und wird dann erneut versucht.
|
|
86
|
+
*/
|
|
87
|
+
export async function iconFuerQuelle(quelle, optionen) {
|
|
88
|
+
if (quelle.art === 'lokal')
|
|
89
|
+
return null;
|
|
90
|
+
const ort = urlOrt(quelle.url);
|
|
91
|
+
if (!ort || ort.host === 'local')
|
|
92
|
+
return null;
|
|
93
|
+
const holen = optionen.fetch ?? ((url, init) => fetch(url, init));
|
|
94
|
+
const timeoutMs = optionen.timeoutMs ?? STANDARD_TIMEOUT_MS;
|
|
95
|
+
const schluessel = `${ort.host}/${ort.besitzer}`;
|
|
96
|
+
const cachePfad = path.join(optionen.cacheVerzeichnis, cacheDateiname(schluessel));
|
|
97
|
+
const cached = await ausCache(cachePfad);
|
|
98
|
+
if (cached !== undefined)
|
|
99
|
+
return cached.wert;
|
|
100
|
+
let ergebnis;
|
|
101
|
+
for (const url of iconKandidaten(ort)) {
|
|
102
|
+
try {
|
|
103
|
+
ergebnis = await mitTimeout(async (signal) => alsDataUrl(await holen(url, { signal })), timeoutMs);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
ergebnis = undefined;
|
|
107
|
+
}
|
|
108
|
+
if (ergebnis)
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
await mkdir(optionen.cacheVerzeichnis, { recursive: true }).catch(() => undefined);
|
|
112
|
+
await writeFile(cachePfad, ergebnis ?? '', 'utf8').catch(() => undefined);
|
|
113
|
+
return ergebnis ?? null;
|
|
114
|
+
}
|
package/dist/gui/main.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// hmgr-GUI, Electron-Main (E40). Baut dieselben Anwendungsfälle wie die CLI (src/cli/main.ts),
|
|
2
|
+
// registriert IPC-Handler für den Renderer und implementiert den Rückfrage-Port als Dialog im
|
|
3
|
+
// Fenster. Kein zweiter Prozess: der Kern läuft direkt im Main-Prozess, die GUI ist ein zweiter
|
|
4
|
+
// Kompositions-Einstieg neben der CLI (ARCHITEKTUR.md „Eingang").
|
|
5
|
+
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { stat } from 'node:fs/promises';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
12
|
+
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
13
|
+
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
14
|
+
import { iconFuerQuelle } from "./icons.js";
|
|
15
|
+
import { entscheideOeffnen } from "./oeffnen.js";
|
|
16
|
+
const hierVerzeichnis = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const smoketest = process.env.HMGR_GUI_SMOKETEST === '1';
|
|
18
|
+
const umgebung = umgebungLesen();
|
|
19
|
+
/** Cache der Quellen-Icons (E60), unterhalb von HMGR_HOME wie der Zustand: Tests/Smoke fassen so
|
|
20
|
+
nie ein echtes Verzeichnis an. */
|
|
21
|
+
const iconCacheVerzeichnis = path.join(umgebung.hmgrHome, 'icon-cache');
|
|
22
|
+
/** Intervall des selbsttätigen Aktualisierens (E44): fest, keine Einstellung. */
|
|
23
|
+
const SECHS_STUNDEN_MS = 6 * 60 * 60 * 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Fake der claude-CLI für den Smoke-Test (E40 R4), nach dem Muster von `fakeClaude` in
|
|
26
|
+
* tests/helfer.ts: nie `code: 0`, also ohne Plugin-Fähigkeit, wie ohne installierte CLI. So fasst
|
|
27
|
+
* ein automatisierter Lauf formal nie die echte claude-CLI an, auch wenn sie auf dem PATH liegt.
|
|
28
|
+
*/
|
|
29
|
+
const smoketestClaudeAusfuehrer = async () => ({ stdout: '', code: 1 });
|
|
30
|
+
let hauptfenster = null;
|
|
31
|
+
/**
|
|
32
|
+
* Rückfrage-Port als Dialog im Renderer (E28, E34): der Main-Prozess schickt die Frage per
|
|
33
|
+
* `webContents.send`, der Renderer zeigt einen <dialog> im Stil des Entwurfs und schickt die
|
|
34
|
+
* Antwort über einen Kanal mit Korrelations-ID zurück. Ohne offenes Fenster kann niemand antworten,
|
|
35
|
+
* dort gilt wie am Terminal ohne TTY: Nein bzw. keine Antwort (E34).
|
|
36
|
+
*/
|
|
37
|
+
class GuiRueckfrage {
|
|
38
|
+
ausstehend = new Map();
|
|
39
|
+
constructor() {
|
|
40
|
+
ipcMain.on('hmgr:rueckfrage:antwort', (_event, nachricht) => {
|
|
41
|
+
const aufloesen = this.ausstehend.get(nachricht.id);
|
|
42
|
+
if (aufloesen === undefined)
|
|
43
|
+
return;
|
|
44
|
+
this.ausstehend.delete(nachricht.id);
|
|
45
|
+
aufloesen(nachricht.antwort);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
stellen(payload) {
|
|
49
|
+
if (hauptfenster === null)
|
|
50
|
+
return Promise.resolve(payload.art === 'bestaetigen' ? false : undefined);
|
|
51
|
+
const id = randomUUID();
|
|
52
|
+
return new Promise((aufloesen) => {
|
|
53
|
+
this.ausstehend.set(id, aufloesen);
|
|
54
|
+
hauptfenster?.webContents.send('hmgr:rueckfrage', { id, ...payload });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async bestaetigen(frage) {
|
|
58
|
+
const antwort = await this.stellen({ art: 'bestaetigen', frage });
|
|
59
|
+
return antwort === true;
|
|
60
|
+
}
|
|
61
|
+
async auswaehlen(frage, optionen) {
|
|
62
|
+
const antwort = await this.stellen({ art: 'auswaehlen', frage, optionen });
|
|
63
|
+
return typeof antwort === 'number' ? antwort : undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rueckfrage = new GuiRueckfrage();
|
|
67
|
+
const { anwendungsfaelle } = bauen(umgebung, {
|
|
68
|
+
rueckfrage,
|
|
69
|
+
...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
|
|
70
|
+
});
|
|
71
|
+
async function auffangen(fn) {
|
|
72
|
+
try {
|
|
73
|
+
return { ok: true, daten: await fn() };
|
|
74
|
+
}
|
|
75
|
+
catch (fehler) {
|
|
76
|
+
return { ok: false, fehler: fehler instanceof Error ? fehler.message : String(fehler) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** ZielZustand über IPC: Maps werden zu einfachen Objekten, der Rest ist schon JSON-tauglich. */
|
|
80
|
+
function zielZustandSerialisieren(z) {
|
|
81
|
+
return {
|
|
82
|
+
ziel: z.ziel,
|
|
83
|
+
verzeichnis: z.verzeichnis,
|
|
84
|
+
eintraege: z.eintraege,
|
|
85
|
+
einschraenkungen: Object.fromEntries(z.einschraenkungen),
|
|
86
|
+
gruende: Object.fromEntries(z.gruende),
|
|
87
|
+
ignoriert: z.ignoriert,
|
|
88
|
+
uebernahme: Object.fromEntries(z.uebernahme),
|
|
89
|
+
istRegistriert: z.istRegistriert,
|
|
90
|
+
kannSkillOrdner: z.kannSkillOrdner,
|
|
91
|
+
...(z.plugins === undefined ? {} : { plugins: z.plugins }),
|
|
92
|
+
...(z.verwalteterSkillOrdner === undefined ? {} : { verwalteterSkillOrdner: z.verwalteterSkillOrdner }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function zielAusIpc(harness, ort) {
|
|
96
|
+
return { harness, ort };
|
|
97
|
+
}
|
|
98
|
+
function registriereIpc() {
|
|
99
|
+
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 zurückgespeist.
|
|
101
|
+
ipcMain.handle('hmgr:home', async () => auffangen(async () => os.homedir()));
|
|
102
|
+
ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand()).map(zielZustandSerialisieren)));
|
|
103
|
+
ipcMain.handle('hmgr:marketplaces', async () => auffangen(async () => (await anwendungsfaelle.marketplaces()).map((m) => ({
|
|
104
|
+
quelleText: quelleAlsText(m.quelle),
|
|
105
|
+
name: m.name,
|
|
106
|
+
plugins: m.plugins,
|
|
107
|
+
}))));
|
|
108
|
+
// Skill-Katalog über alle bekannten Quellen (E40 R3): über den Anwendungsfall `katalog`, nicht
|
|
109
|
+
// mehr direkt über die Ports. Eine nicht erreichbare Quelle trägt einen Fehlervermerk (E12-Muster
|
|
110
|
+
// wie istZustand/marketplaces), ihre Skills fehlen einfach in der flachen Liste.
|
|
111
|
+
ipcMain.handle('hmgr:skills', async () => auffangen(async () => {
|
|
112
|
+
const ergebnis = [];
|
|
113
|
+
for (const eintrag of await anwendungsfaelle.katalog()) {
|
|
114
|
+
if (eintrag.ref === undefined)
|
|
115
|
+
continue; // nicht erreichbar, keine Skills
|
|
116
|
+
for (const skill of eintrag.skills)
|
|
117
|
+
ergebnis.push({
|
|
118
|
+
name: skill.name,
|
|
119
|
+
pfad: skill.pfad,
|
|
120
|
+
...(skill.beschreibung === undefined ? {} : { beschreibung: skill.beschreibung }),
|
|
121
|
+
...(skill.nurPerBefehl === undefined ? {} : { nurPerBefehl: skill.nurPerBefehl }),
|
|
122
|
+
...(skill.nichtPerBefehl === undefined ? {} : { nichtPerBefehl: skill.nichtPerBefehl }),
|
|
123
|
+
...(skill.registriertHooks === undefined ? {} : { registriertHooks: skill.registriertHooks }),
|
|
124
|
+
...(skill.werkzeugeErlaubt === undefined ? {} : { werkzeugeErlaubt: skill.werkzeugeErlaubt }),
|
|
125
|
+
...(skill.kompatibilitaet === undefined ? {} : { kompatibilitaet: skill.kompatibilitaet }),
|
|
126
|
+
...(skill.modell === undefined ? {} : { modell: skill.modell }),
|
|
127
|
+
...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
|
|
128
|
+
...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
|
|
129
|
+
quelleText: quelleAlsText(eintrag.quelle),
|
|
130
|
+
quelleName: quelleName(eintrag.quelle),
|
|
131
|
+
quelleSchluessel: quelleSchluessel(eintrag.quelle),
|
|
132
|
+
ref: eintrag.ref,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return ergebnis;
|
|
136
|
+
}));
|
|
137
|
+
// Nur die Herkunft, ohne die Quellen zu lesen: der Katalog daneben tut das schon.
|
|
138
|
+
ipcMain.handle('hmgr:quellen', async () => auffangen(async () => (await anwendungsfaelle.quellen()).map((quelle) => ({
|
|
139
|
+
art: quelle.art,
|
|
140
|
+
text: quelleAlsText(quelle),
|
|
141
|
+
name: quelleName(quelle),
|
|
142
|
+
}))));
|
|
143
|
+
// Quelle-Verwaltung in den Einstellungen (E54, E59): Text-zu-Quelle passiert im Main-Prozess, der
|
|
144
|
+
// Renderer kennt nur Zeichenketten. Der Anzeigename kommt serverseitig mit, damit dem Renderer nie
|
|
145
|
+
// ein weggefiltertes Feld fehlt (E59: „name" ist reine Anzeige, geht nicht in quelleAusText ein).
|
|
146
|
+
ipcMain.handle('hmgr:quelleHinzufuegen', async (_event, text, name) => auffangen(async () => {
|
|
147
|
+
const ergebnis = await anwendungsfaelle.quelleHinzufuegen(quelleAusText(text), name);
|
|
148
|
+
return {
|
|
149
|
+
quelle: {
|
|
150
|
+
art: ergebnis.quelle.art,
|
|
151
|
+
text: quelleAlsText(ergebnis.quelle),
|
|
152
|
+
name: quelleName(ergebnis.quelle),
|
|
153
|
+
},
|
|
154
|
+
skills: ergebnis.skills,
|
|
155
|
+
marketplace: ergebnis.marketplace,
|
|
156
|
+
bekannt: ergebnis.bekannt,
|
|
157
|
+
};
|
|
158
|
+
}));
|
|
159
|
+
ipcMain.handle('hmgr:quelleEntfernen', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleEntfernen(quelleAusText(text))));
|
|
160
|
+
ipcMain.handle('hmgr:quelleUmbenennen', async (_event, text, name) => auffangen(async () => {
|
|
161
|
+
const umbenannt = await anwendungsfaelle.quelleUmbenennen(quelleAusText(text), name);
|
|
162
|
+
return { art: umbenannt.art, text: quelleAlsText(umbenannt), name: quelleName(umbenannt) };
|
|
163
|
+
}));
|
|
164
|
+
// Aufräum-Angebot nach dem Entfernen einer Quelle (E58): Inventar vor dem eigentlichen Entfernen
|
|
165
|
+
// erheben (der Renderer ruft das, bevor er quelleEntfernen aufruft), dann bei Zustimmung abräumen.
|
|
166
|
+
// Beide Ergebnisse reichen unverändert durch: schon reine Daten, keine Maps oder Funktionen darin.
|
|
167
|
+
ipcMain.handle('hmgr:quelleAufraeumInventar', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleAufraeumInventar(quelleAusText(text))));
|
|
168
|
+
ipcMain.handle('hmgr:quelleAufraeumen', async (_event, inventar) => auffangen(async () => anwendungsfaelle.quelleAufraeumen(inventar)));
|
|
169
|
+
// Die GUI arbeitet an einem Arbeitsverzeichnis wie die CLI mit --local (E57, F32): Links werden
|
|
170
|
+
// lokal gelegt und entfernt, der Zeiger bleibt unangetastet, der Kern hängt bei einer bestehenden
|
|
171
|
+
// Bindung von sich aus einen Hinweis an. Für Home und Profil ist `lokal` folgenlos (E26).
|
|
172
|
+
ipcMain.handle('hmgr:linkLegen', async (_event, args) => auffangen(async () => anwendungsfaelle.linkLegen({
|
|
173
|
+
skill: args.skill,
|
|
174
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
175
|
+
...(args.quelle === undefined ? {} : { quelle: args.quelle }),
|
|
176
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
177
|
+
})));
|
|
178
|
+
ipcMain.handle('hmgr:linkEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.linkEntfernen({
|
|
179
|
+
skill: args.skill,
|
|
180
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
181
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
182
|
+
})));
|
|
183
|
+
ipcMain.handle('hmgr:eintragIgnorieren', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragIgnorieren({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
184
|
+
ipcMain.handle('hmgr:eintragBeachten', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragBeachten({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
185
|
+
// Auch die Übernahme legt Links und schriebe ohne `lokal` den Zeiger (E31 über linkAusFund), sie
|
|
186
|
+
// trifft also dieselbe Sperre wie link (E57). In der GUI arbeitet sie darum wie die CLI mit
|
|
187
|
+
// --local (F32), sonst bräche der Knopf an jedem gebundenen Arbeitsverzeichnis ab.
|
|
188
|
+
ipcMain.handle('hmgr:uebernehmen', async (_event, args) => auffangen(async () => anwendungsfaelle.uebernehmen({
|
|
189
|
+
ziel: zielAusIpc(args.harness, args.ort),
|
|
190
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
191
|
+
probelauf: args.probelauf,
|
|
192
|
+
lokal: args.ort.art === 'arbeitsverzeichnis',
|
|
193
|
+
})));
|
|
194
|
+
ipcMain.handle('hmgr:eintragLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.eintragLoeschen({ ziel: zielAusIpc(args.harness, args.ort), name: args.name })));
|
|
195
|
+
ipcMain.handle('hmgr:aktualisieren', async (_event, args) => auffangen(async () => anwendungsfaelle.aktualisieren(args)));
|
|
196
|
+
ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
|
|
197
|
+
ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
|
|
198
|
+
// Nativer Verzeichnis-Dialog fürs Pfadfeld in „Verzeichnis hinzufügen" (A1). Im Smoke-Test darf
|
|
199
|
+
// nie ein echter Systemdialog aufgehen: sofort null, wie bei Abbruch.
|
|
200
|
+
ipcMain.handle('hmgr:verzeichnisWaehlen', async () => auffangen(async () => {
|
|
201
|
+
if (smoketest || hauptfenster === null)
|
|
202
|
+
return null;
|
|
203
|
+
const ergebnis = await dialog.showOpenDialog(hauptfenster, {
|
|
204
|
+
properties: ['openDirectory', 'createDirectory'],
|
|
205
|
+
});
|
|
206
|
+
if (ergebnis.canceled || ergebnis.filePaths.length === 0)
|
|
207
|
+
return null;
|
|
208
|
+
return ergebnis.filePaths[0] ?? null;
|
|
209
|
+
}));
|
|
210
|
+
// Harness-Schalter im Einstellungen-Modal (E38, GRILL.md Entwurfskritik 4).
|
|
211
|
+
ipcMain.handle('hmgr:harnessTypen', async () => auffangen(async () => anwendungsfaelle.harnessTypen()));
|
|
212
|
+
ipcMain.handle('hmgr:aktiveHarnesses', async () => auffangen(async () => anwendungsfaelle.aktiveHarnesses()));
|
|
213
|
+
// Für den Vermerk „nicht installiert" an den Schaltern (E42).
|
|
214
|
+
ipcMain.handle('hmgr:nichtVorhandeneHarnesses', async () => auffangen(async () => anwendungsfaelle.nichtVorhandeneHarnesses()));
|
|
215
|
+
ipcMain.handle('hmgr:harnessAktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessAktivieren(name)));
|
|
216
|
+
ipcMain.handle('hmgr:harnessDeaktivieren', async (_event, name) => auffangen(async () => anwendungsfaelle.harnessDeaktivieren(name)));
|
|
217
|
+
ipcMain.handle('hmgr:saeubern', async (_event, name) => auffangen(async () => anwendungsfaelle.saeubern(name)));
|
|
218
|
+
// Verwalteter Hermes-Skill-Ordner (E56): Zeigen kommt schon über hmgr:istZustand, hier nur Ändern.
|
|
219
|
+
ipcMain.handle('hmgr:skillOrdnerFestlegen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerFestlegen(zielAusIpc(args.harness, args.ort), args.pfad)));
|
|
220
|
+
ipcMain.handle('hmgr:skillOrdnerEntfernen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerEntfernen(zielAusIpc(args.harness, args.ort))));
|
|
221
|
+
ipcMain.handle('hmgr:skillOrdnerLoeschen', async (_event, args) => auffangen(async () => anwendungsfaelle.skillOrdnerLoeschen(zielAusIpc(args.harness, args.ort), args.pfad)));
|
|
222
|
+
// Quellen-Icon (E60): nur im Main-Prozess geladen, als data-URL geliefert (CSP erlaubt img-src
|
|
223
|
+
// data:). Im Smoke-Test nie ein Netzzugriff: sofort null, der Renderer zeigt dann das generische
|
|
224
|
+
// Symbol. Ein Fehlschlag wirft nie (auffangen fängt ihn, iconFuerQuelle selbst wirft ohnehin nicht).
|
|
225
|
+
ipcMain.handle('hmgr:icon', async (_event, quelleText) => auffangen(async () => {
|
|
226
|
+
if (smoketest)
|
|
227
|
+
return null;
|
|
228
|
+
return iconFuerQuelle(quelleAusText(quelleText), { cacheVerzeichnis: iconCacheVerzeichnis });
|
|
229
|
+
}));
|
|
230
|
+
// Quelle öffnen (E60): die Protokoll-Weiche steht in oeffnen.ts (rein, testbar), hier nur die
|
|
231
|
+
// Wirkung. `shell.openExternal` bekommt ausschließlich http(s)-URLs, `shell.openPath` nur einen
|
|
232
|
+
// Ordner, den es wirklich gibt — nichts anderes wird zugelassen. Alles, was es gibt, aber kein
|
|
233
|
+
// Ordner ist, wird im Finder nur gezeigt, nicht geöffnet: `openPath` startet sonst eine Datei
|
|
234
|
+
// mit ihrem Standardprogramm, und ein Pfad aus dem Renderer soll nichts starten können.
|
|
235
|
+
ipcMain.handle('hmgr:quelleOeffnen', async (_event, quelleText) => auffangen(async () => {
|
|
236
|
+
const entscheidung = entscheideOeffnen(quelleAusText(quelleText));
|
|
237
|
+
if (entscheidung.art === 'browser')
|
|
238
|
+
await shell.openExternal(entscheidung.url);
|
|
239
|
+
else if (entscheidung.art === 'ordner') {
|
|
240
|
+
const info = await stat(entscheidung.pfad).catch(() => undefined);
|
|
241
|
+
if (info === undefined)
|
|
242
|
+
throw new Error(`Es gibt „${entscheidung.pfad}" nicht (mehr).`);
|
|
243
|
+
if (info.isDirectory())
|
|
244
|
+
await shell.openPath(entscheidung.pfad);
|
|
245
|
+
else
|
|
246
|
+
shell.showItemInFolder(entscheidung.pfad);
|
|
247
|
+
}
|
|
248
|
+
return entscheidung;
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
function fensterOeffnen() {
|
|
252
|
+
hauptfenster = new BrowserWindow({
|
|
253
|
+
width: 1280,
|
|
254
|
+
height: 860,
|
|
255
|
+
minWidth: 860,
|
|
256
|
+
minHeight: 560,
|
|
257
|
+
backgroundColor: '#E3E7E0', // --platte, verhindert einen weißen Blitz beim Start
|
|
258
|
+
// Im Smoke-Test bleibt das Fenster unsichtbar: der Renderer lädt trotzdem vollständig, der
|
|
259
|
+
// DOM-Auszug kommt per executeJavaScript. So klaut ein automatisierter Lauf niemandem den
|
|
260
|
+
// Eingabefokus und öffnet kein sichtbares Fenster.
|
|
261
|
+
show: !smoketest,
|
|
262
|
+
webPreferences: {
|
|
263
|
+
preload: path.join(hierVerzeichnis, 'preload.cjs'),
|
|
264
|
+
contextIsolation: true,
|
|
265
|
+
nodeIntegration: false,
|
|
266
|
+
sandbox: true,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
hauptfenster.on('closed', () => {
|
|
270
|
+
hauptfenster = null;
|
|
271
|
+
});
|
|
272
|
+
// Renderer-Konsole im Main-Log, für Fehlersuche und den Smoke-Test.
|
|
273
|
+
hauptfenster.webContents.on('console-message', (event) => {
|
|
274
|
+
console.log(`[renderer] ${event.message}`);
|
|
275
|
+
});
|
|
276
|
+
// Nur für den Smoke-Test (scripts/gui-smoke-test.ts): ein DOM-Auszug im Log, danach Ende.
|
|
277
|
+
if (smoketest) {
|
|
278
|
+
const fenster = hauptfenster;
|
|
279
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
void fenster.webContents
|
|
282
|
+
.executeJavaScript(`(async () => {
|
|
283
|
+
// Einstellungen-Modal (E40 R2): öffnen, die Harness-Schalter ablesen, wieder schließen.
|
|
284
|
+
document.getElementById('einstellungen')?.click();
|
|
285
|
+
var einstellungenOffen = document.getElementById('d-einstellungen')?.open === true;
|
|
286
|
+
var harnessSchalter = Array.from(document.querySelectorAll('#harnessliste input[type=checkbox]'))
|
|
287
|
+
.map((i) => i.dataset.harness + ':' + i.checked)
|
|
288
|
+
.join(',');
|
|
289
|
+
// Quellen-Abschnitt der Einstellungen (E54): Knopf und Dialoge vorhanden, der
|
|
290
|
+
// Hinzufügen-Dialog lässt sich öffnen und per Abbrechen ohne Fehler wieder schließen.
|
|
291
|
+
// Kein echter Quellen-Lauf: der Abbrechen-Knopf löst keinen IPC-Aufruf aus.
|
|
292
|
+
var quelleneuVorhanden = document.getElementById('quelleneu') !== null;
|
|
293
|
+
var dialogeQuelleVorhanden =
|
|
294
|
+
document.getElementById('d-quelle') !== null && document.getElementById('d-quelle-vergessen') !== null;
|
|
295
|
+
var quelleFehler = null;
|
|
296
|
+
var dQuelleOffenNachOeffnen = false;
|
|
297
|
+
var dQuelleOffenNachAbbrechen = true;
|
|
298
|
+
try {
|
|
299
|
+
document.getElementById('quelleneu')?.click();
|
|
300
|
+
dQuelleOffenNachOeffnen = document.getElementById('d-quelle')?.open === true;
|
|
301
|
+
document.querySelector('#d-quelle button[value="abbruch"]')?.click();
|
|
302
|
+
dQuelleOffenNachAbbrechen = document.getElementById('d-quelle')?.open === true;
|
|
303
|
+
} catch (fehler) {
|
|
304
|
+
quelleFehler = String(fehler);
|
|
305
|
+
}
|
|
306
|
+
// Aufräum-Angebot nach Quelle entfernen (E58): kein echter Lauf (der würde Checkouts
|
|
307
|
+
// und Links wirklich löschen), nur ob der Dialog existiert und sich öffnen und über
|
|
308
|
+
// Behalten schließen lässt, ohne Fehler.
|
|
309
|
+
var dialogQuelleAufraeumenVorhanden = document.getElementById('d-quelle-aufraeumen') !== null;
|
|
310
|
+
var quelleAufraeumenFehler = null;
|
|
311
|
+
var dQuelleAufraeumenOffenNachOeffnen = false;
|
|
312
|
+
var dQuelleAufraeumenOffenNachBehalten = true;
|
|
313
|
+
try {
|
|
314
|
+
var dQuelleAufraeumen = document.getElementById('d-quelle-aufraeumen');
|
|
315
|
+
dQuelleAufraeumen?.showModal();
|
|
316
|
+
dQuelleAufraeumenOffenNachOeffnen = dQuelleAufraeumen?.open === true;
|
|
317
|
+
document.querySelector('#d-quelle-aufraeumen button[value="behalten"]')?.click();
|
|
318
|
+
dQuelleAufraeumenOffenNachBehalten = dQuelleAufraeumen?.open === true;
|
|
319
|
+
} catch (fehler) {
|
|
320
|
+
quelleAufraeumenFehler = String(fehler);
|
|
321
|
+
}
|
|
322
|
+
// Anzeigename der Quelle (E59): die Einstellungen-Liste zeigt den abgeleiteten Namen
|
|
323
|
+
// „skills-repo" statt der vollen Fixture-URL, das Namensfeld im Hinzufügen-Dialog gibt
|
|
324
|
+
// es zusätzlich zum URL/Pfad-Feld.
|
|
325
|
+
var quellenlisteNamen = Array.from(document.querySelectorAll('#quellenliste b'))
|
|
326
|
+
.map((b) => b.textContent)
|
|
327
|
+
.join(',');
|
|
328
|
+
var dqNameVorhanden = document.getElementById('dq-name') !== null;
|
|
329
|
+
// Umbenennen (E59): der Knopf je Quelle öffnet den Dialog mit dem aktuellen Namen im
|
|
330
|
+
// Feld, Abbrechen schließt ihn wieder. Beide Knöpfe der Zeile liegen zusammen in
|
|
331
|
+
// einem Kasten, sonst lägen sie im Grid übereinander.
|
|
332
|
+
var quellenknoepfeZahl = document.querySelectorAll('#quellenliste .quellenknoepfe').length;
|
|
333
|
+
var quelleUmbenennenFehler = null;
|
|
334
|
+
var dQuUOffenNachOeffnen = false;
|
|
335
|
+
var dQuUName = null;
|
|
336
|
+
var dQuUOffenNachAbbrechen = true;
|
|
337
|
+
try {
|
|
338
|
+
var dQuU = document.getElementById('d-quelle-umbenennen');
|
|
339
|
+
// E60: der Umbenennen-Knopf ist jetzt ein Stift-Icon-Knopf ohne Text (Klasse
|
|
340
|
+
// „umbenennen", aria-label statt Beschriftung).
|
|
341
|
+
document.querySelector('#quellenliste .umbenennen')?.click();
|
|
342
|
+
dQuUOffenNachOeffnen = dQuU?.open === true;
|
|
343
|
+
dQuUName = document.getElementById('dqu-name')?.value ?? null;
|
|
344
|
+
document.querySelector('#d-quelle-umbenennen button[value="abbruch"]')?.click();
|
|
345
|
+
dQuUOffenNachAbbrechen = dQuU?.open === true;
|
|
346
|
+
} catch (fehler) {
|
|
347
|
+
quelleUmbenennenFehler = String(fehler);
|
|
348
|
+
}
|
|
349
|
+
document.getElementById('d-einstellungen')?.close();
|
|
350
|
+
// Konfigzeile am Hermes-Ziel (E56): der verwaltete Pfad steht in der Karte, auch
|
|
351
|
+
// ungeklappt (hidden ändert textContent nicht). Dazu der Festlegen-Weg am unmanaged
|
|
352
|
+
// Profil, Öffnen und Abbrechen ohne Fehler, nach dem Muster der d-quelle-Prüfungen.
|
|
353
|
+
var konfigzeileEl = document.querySelector('.konfigzeile');
|
|
354
|
+
var konfigzeileText = konfigzeileEl ? konfigzeileEl.textContent.replace(/\\s+/g, ' ').trim() : null;
|
|
355
|
+
var skillOrdnerFehler = null;
|
|
356
|
+
var dSkillordnerOffenNachOeffnen = false;
|
|
357
|
+
var dSkillordnerOffenNachAbbrechen = true;
|
|
358
|
+
try {
|
|
359
|
+
var festlegenKnopf = Array.from(document.querySelectorAll('button'))
|
|
360
|
+
.find((b) => b.textContent === 'Skill-Ordner festlegen …');
|
|
361
|
+
festlegenKnopf?.click();
|
|
362
|
+
dSkillordnerOffenNachOeffnen = document.getElementById('d-skillordner')?.open === true;
|
|
363
|
+
document.querySelector('#d-skillordner button[value="abbruch"]')?.click();
|
|
364
|
+
dSkillordnerOffenNachAbbrechen = document.getElementById('d-skillordner')?.open === true;
|
|
365
|
+
} catch (fehler) {
|
|
366
|
+
skillOrdnerFehler = String(fehler);
|
|
367
|
+
}
|
|
368
|
+
// E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
|
|
369
|
+
// Zahnrad statt Sonne am Einstellungen-Knopf.
|
|
370
|
+
var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
|
|
371
|
+
var zielManuellVorhanden = (document.getElementById('zielliste')?.textContent ?? '').includes('manuell');
|
|
372
|
+
var zahnradVorhanden = (document.querySelector('#einstellungen svg')?.innerHTML ?? '').includes('6.47 3.56');
|
|
373
|
+
// Katalog (E40 R3, R5): Beschreibung aus dem Fixture muss in der Skill-Zeile stehen.
|
|
374
|
+
document.querySelector('[data-ansicht="katalog"]')?.click();
|
|
375
|
+
var katalogBeschreibung = document.querySelector('#katalog .zeile .beschreibung')?.textContent ?? null;
|
|
376
|
+
// Besonderheiten aus dem Frontmatter (E55): Marken an der Zeile, Details erst nach
|
|
377
|
+
// einem Klick auf den Aufklapp-Knopf.
|
|
378
|
+
var katalogMarken = document.querySelector('#katalog .zeile .marken')?.textContent
|
|
379
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
380
|
+
var aufklapp = document.querySelector('#katalog .zeile .aufklappknopf');
|
|
381
|
+
var katalogDetailsZu = document.querySelector('#katalog .zeile .skillkoerper')?.hidden ?? null;
|
|
382
|
+
aufklapp?.click();
|
|
383
|
+
var katalogAufgeklappt = aufklapp?.getAttribute('aria-expanded') ?? null;
|
|
384
|
+
var katalogDetails = document.querySelector('#katalog .zeile .skillkoerper:not([hidden])')?.textContent
|
|
385
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
386
|
+
// Sicherheitsrelevante Marken (F11): Hooks und Werkzeuge tragen die warnende Klasse.
|
|
387
|
+
var katalogMarkenWarnend = Array.from(document.querySelectorAll('#katalog .zeile .merker.warnend'))
|
|
388
|
+
.map((m) => m.textContent).join(',');
|
|
389
|
+
// Skills-hinzufügen-Dialog (F13): dieselben Marken an der Wahlzeile.
|
|
390
|
+
var dskKnopf = Array.from(document.querySelectorAll('button'))
|
|
391
|
+
.find((b) => b.textContent === 'Skills hinzufügen');
|
|
392
|
+
var dskMarken = null;
|
|
393
|
+
if (dskKnopf) {
|
|
394
|
+
dskKnopf.click();
|
|
395
|
+
dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
|
|
396
|
+
?.replace(/\\s+/g, ' ').trim() ?? null;
|
|
397
|
+
document.getElementById('d-skills')?.close();
|
|
398
|
+
}
|
|
399
|
+
// Schriften aus fonts/ (Zusatzpunkt 6): erzwingt das Laden und belegt es danach.
|
|
400
|
+
await Promise.all([
|
|
401
|
+
document.fonts.load('400 16px Inter'),
|
|
402
|
+
document.fonts.load('500 16px "JetBrains Mono"'),
|
|
403
|
+
]);
|
|
404
|
+
return JSON.stringify({
|
|
405
|
+
titel: document.title,
|
|
406
|
+
kopfzahlen: document.getElementById('kopfzahlen')?.textContent,
|
|
407
|
+
zielliste: document.getElementById('zielliste')?.textContent?.replace(/\\s+/g, ' ').trim().slice(0, 600),
|
|
408
|
+
meldung: document.getElementById('meldungtext')?.textContent,
|
|
409
|
+
einstellungenOffen,
|
|
410
|
+
harnessSchalter,
|
|
411
|
+
zielBeschreibung,
|
|
412
|
+
zielManuellVorhanden,
|
|
413
|
+
zahnradVorhanden,
|
|
414
|
+
katalogBeschreibung,
|
|
415
|
+
katalogMarken,
|
|
416
|
+
katalogDetailsZu,
|
|
417
|
+
katalogAufgeklappt,
|
|
418
|
+
katalogDetails,
|
|
419
|
+
katalogMarkenWarnend,
|
|
420
|
+
dskMarken,
|
|
421
|
+
interGeladen: document.fonts.check('400 16px Inter'),
|
|
422
|
+
monoGeladen: document.fonts.check('500 16px "JetBrains Mono"'),
|
|
423
|
+
quelleneuVorhanden,
|
|
424
|
+
dialogeQuelleVorhanden,
|
|
425
|
+
quellenlisteNamen,
|
|
426
|
+
dqNameVorhanden,
|
|
427
|
+
quellenknoepfeZahl,
|
|
428
|
+
dQuUOffenNachOeffnen,
|
|
429
|
+
dQuUName,
|
|
430
|
+
dQuUOffenNachAbbrechen,
|
|
431
|
+
quelleUmbenennenFehler,
|
|
432
|
+
dQuelleOffenNachOeffnen,
|
|
433
|
+
dQuelleOffenNachAbbrechen,
|
|
434
|
+
quelleFehler,
|
|
435
|
+
dialogQuelleAufraeumenVorhanden,
|
|
436
|
+
dQuelleAufraeumenOffenNachOeffnen,
|
|
437
|
+
dQuelleAufraeumenOffenNachBehalten,
|
|
438
|
+
quelleAufraeumenFehler,
|
|
439
|
+
// Hinweiszeile im Erfolgsdialog (E26, F32): vorhanden und im Ruhezustand leer.
|
|
440
|
+
deHinweisVorhanden: document.getElementById('de-hinweis') !== null,
|
|
441
|
+
deHinweisVerborgen: document.getElementById('de-hinweis')?.hidden === true,
|
|
442
|
+
konfigzeileText,
|
|
443
|
+
dSkillordnerOffenNachOeffnen,
|
|
444
|
+
dSkillordnerOffenNachAbbrechen,
|
|
445
|
+
skillOrdnerFehler,
|
|
446
|
+
});
|
|
447
|
+
})()`)
|
|
448
|
+
.then((auszug) => console.log(`[smoke] DOM-Auszug: ${auszug}`))
|
|
449
|
+
.finally(() => app.quit());
|
|
450
|
+
}, 3000);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Selbsttätiges Aktualisieren (E44): einmal, sobald der Renderer bereit ist, danach alle sechs
|
|
454
|
+
// Stunden, solange das Fenster offen bleibt. Im Smoke-Test nie, sonst würde ein automatisierter
|
|
455
|
+
// Lauf einen echten Git-Fetch anstoßen.
|
|
456
|
+
if (!smoketest) {
|
|
457
|
+
const fenster = hauptfenster;
|
|
458
|
+
fenster.webContents.once('did-finish-load', () => {
|
|
459
|
+
fenster.webContents.send('hmgr:aktualisierenAnstossen');
|
|
460
|
+
});
|
|
461
|
+
const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen'), SECHS_STUNDEN_MS);
|
|
462
|
+
fenster.on('closed', () => clearInterval(intervall));
|
|
463
|
+
}
|
|
464
|
+
void hauptfenster.loadFile(path.join(hierVerzeichnis, 'renderer', 'index.html'));
|
|
465
|
+
}
|
|
466
|
+
registriereIpc();
|
|
467
|
+
app.whenReady().then(() => {
|
|
468
|
+
// Im Smoke-Test kein Dock-Icon und kein Fokuswechsel: das Fenster bleibt unsichtbar (show:false
|
|
469
|
+
// oben), macOS würde aber ohne diese Zeile trotzdem ein Dock-Icon zeigen und aktivieren.
|
|
470
|
+
if (smoketest && process.platform === 'darwin')
|
|
471
|
+
app.dock?.hide();
|
|
472
|
+
fensterOeffnen();
|
|
473
|
+
app.on('activate', () => {
|
|
474
|
+
if (BrowserWindow.getAllWindows().length === 0)
|
|
475
|
+
fensterOeffnen();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
app.on('window-all-closed', () => {
|
|
479
|
+
if (process.platform !== 'darwin')
|
|
480
|
+
app.quit();
|
|
481
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Entscheidung, wie ein Klick auf eine Quelle sie öffnet (E60). Reine Funktion, ohne Electron:
|
|
2
|
+
// der IPC-Handler in main.ts ruft sie und reicht das Ergebnis an `shell.openExternal` (nur für
|
|
3
|
+
// http/https) oder `shell.openPath` (lokaler Ordner) weiter — nichts anderes wird zugelassen,
|
|
4
|
+
// kein beliebiges Protokoll geht an eine der beiden Electron-Funktionen.
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { urlOrt } from "../kern/quelle-ort.js";
|
|
7
|
+
/**
|
|
8
|
+
* http(s)-Quellen öffnen ihre Adresse direkt im Browser. Eine SSH- oder scp-Adresse
|
|
9
|
+
* (`git@host:besitzer/repo.git`) hat keine Webseite als Adresse, aber Host, Besitzer und Repo sind
|
|
10
|
+
* bekannt (E36), also wird daraus die https-Repo-Seite gebaut — `shell.openExternal` bekommt so nie
|
|
11
|
+
* die ssh-Adresse selbst zu sehen. Lokale Quellen und `file://`-Quellen öffnen ihren Pfad im
|
|
12
|
+
* Finder. Alles, woraus sich weder eine http(s)-Adresse noch ein Pfad gewinnen lässt, wird
|
|
13
|
+
* abgelehnt: kein anderes Protokoll geht an eine Electron-Shell-Funktion.
|
|
14
|
+
*/
|
|
15
|
+
export function entscheideOeffnen(quelle) {
|
|
16
|
+
if (quelle.art === 'lokal')
|
|
17
|
+
return { art: 'ordner', pfad: quelle.pfad };
|
|
18
|
+
const text = quelle.url.trim();
|
|
19
|
+
if (/^https?:\/\//i.test(text))
|
|
20
|
+
return { art: 'browser', url: text };
|
|
21
|
+
if (/^file:\/\//i.test(text)) {
|
|
22
|
+
try {
|
|
23
|
+
return { art: 'ordner', pfad: fileURLToPath(text) };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { art: 'abgelehnt' };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const ort = urlOrt(text);
|
|
30
|
+
if (ort && ort.host !== 'local')
|
|
31
|
+
return { art: 'browser', url: `https://${ort.host}/${ort.besitzer}/${ort.repo}` };
|
|
32
|
+
return { art: 'abgelehnt' };
|
|
33
|
+
}
|