hmgr 0.0.4 → 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 +251 -23
- package/dist/adapter/composer.js +4 -3
- package/dist/adapter/datei-text.js +18 -0
- package/dist/adapter/dateisystem.js +146 -8
- package/dist/adapter/git.js +129 -17
- package/dist/adapter/hermes.js +235 -10
- 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 +96 -19
- package/dist/adapter/terminal.js +1 -1
- package/dist/adapter/umgebung.js +43 -0
- package/dist/adapter/vergleich.js +20 -0
- package/dist/adapter/zustand-json.js +6 -1
- package/dist/cli/hilfe.js +227 -26
- package/dist/cli/main.js +362 -20
- package/dist/gui/beobachter.js +108 -0
- package/dist/gui/icons.js +114 -0
- package/dist/gui/login-pfad.js +120 -0
- package/dist/gui/main.js +823 -0
- package/dist/gui/mcp-ansicht.js +57 -0
- package/dist/gui/oeffnen.js +33 -0
- package/dist/gui/preload.cjs +70 -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 +581 -0
- package/dist/gui/renderer/renderer.js +3227 -0
- package/dist/gui/renderer/styles.css +701 -0
- package/dist/kern/anwendungsfaelle.js +859 -65
- package/dist/kern/modell.js +297 -0
- package/dist/kern/quelle-ort.js +14 -1
- package/dist/ports/harness.js +16 -0
- package/dist/ports/mcp-katalog.js +1 -0
- package/dist/ports/umgebung.js +1 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +47 -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
|
+
}
|
|
@@ -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
|
+
}
|