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
package/dist/kern/modell.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Begriffe aus CONTEXT.md. Nur Werte und Regeln, keine Pfade zu Dateien außerhalb des Modells.
|
|
2
|
+
import { ortSchluessel, urlOrt } from "./quelle-ort.js";
|
|
2
3
|
/** Ansprache eines Plugins im Zeiger und gegenüber der claude-CLI: `<plugin>@<marketplace>`. */
|
|
3
4
|
export function pluginSchluessel(plugin) {
|
|
4
5
|
return `${plugin.name}@${plugin.marketplace}`;
|
|
@@ -45,8 +46,80 @@ export function zielSchluessel(ziel) {
|
|
|
45
46
|
const ort = o.art === 'home' ? 'home' : o.art === 'profil' ? `profil:${o.name}` : `dir:${o.pfad}`;
|
|
46
47
|
return `${ziel.harness}/${ort}`;
|
|
47
48
|
}
|
|
49
|
+
/** Wie eine Quelle geschrieben steht: ihre URL oder ihr Pfad. Für Ausgaben, nicht für Vergleiche. */
|
|
50
|
+
export function quelleAlsText(quelle) {
|
|
51
|
+
return quelle.art === 'git' ? quelle.url : quelle.pfad;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Identität einer Quelle (E36): bei einer Git-Quelle Host, Besitzer und Repo, nicht die
|
|
55
|
+
* Schreibweise der URL. SSH- und HTTPS-Form desselben Repos sind dieselbe Quelle, mit und ohne
|
|
56
|
+
* `.git`-Suffix ebenso, und der Host zählt ohne Rücksicht auf Groß- und Kleinschreibung. Ein Text,
|
|
57
|
+
* der sich nicht als URL lesen lässt, bleibt sein eigener Schlüssel. Lokale Quellen bleiben ihr
|
|
58
|
+
* Pfad (B11).
|
|
59
|
+
*/
|
|
48
60
|
export function quelleSchluessel(quelle) {
|
|
49
|
-
|
|
61
|
+
if (quelle.art === 'lokal')
|
|
62
|
+
return `lokal:${quelle.pfad}`;
|
|
63
|
+
const ort = urlOrt(quelle.url);
|
|
64
|
+
return ort === undefined ? quelle.url : ortSchluessel(ort);
|
|
65
|
+
}
|
|
66
|
+
/** Letztes nicht leeres Segment eines Pfads oder einer URL, ohne abschließende Schrägstriche. */
|
|
67
|
+
function letztesSegment(text) {
|
|
68
|
+
const bereinigt = text.replace(/\/+$/, '');
|
|
69
|
+
const segmente = bereinigt.split('/').filter((s) => s.length > 0);
|
|
70
|
+
return segmente[segmente.length - 1] ?? bereinigt;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Abgeleiteter Anzeigename einer Quelle (E59), ohne Rücksicht auf einen gespeicherten Namen: bei
|
|
74
|
+
* einer Git-Quelle das letzte Pfadsegment der URL ohne `.git`-Suffix (über `urlOrt`, wie E36 den
|
|
75
|
+
* Repo-Namen schon für die Identität liest), bei einer lokalen Quelle ihr Ordnername.
|
|
76
|
+
*/
|
|
77
|
+
export function quelleNameAbleiten(quelle) {
|
|
78
|
+
if (quelle.art === 'lokal')
|
|
79
|
+
return letztesSegment(quelle.pfad);
|
|
80
|
+
const ort = urlOrt(quelle.url);
|
|
81
|
+
return ort !== undefined ? ort.repo : letztesSegment(quelle.url);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Anzeigename einer Quelle (E59): der gespeicherte Name, sonst der abgeleitete. Reine Anzeige,
|
|
85
|
+
* geht nicht in `quelleSchluessel` ein. CLI und GUI nutzen ausschließlich diesen Helfer.
|
|
86
|
+
*/
|
|
87
|
+
export function quelleName(quelle) {
|
|
88
|
+
if (quelle.name !== undefined && quelle.name.trim().length > 0)
|
|
89
|
+
return quelle.name;
|
|
90
|
+
return quelleNameAbleiten(quelle);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Ein Anzeigename, den noch keine der genannten Quellen trägt (E59): der Wunschname, sonst mit
|
|
94
|
+
* angehängter Zahl. Nur für Wege, die eine Quelle nebenbei anlegen (Übernahme, E31): dort gibt es
|
|
95
|
+
* niemanden, der eine Kollision auflösen könnte, und ein Abbruch ließe die Übernahme ohne Ausweg
|
|
96
|
+
* scheitern. Wo der Nutzer den Namen selbst nennt, bleibt eine Kollision ein Fehler.
|
|
97
|
+
*/
|
|
98
|
+
export function quelleNameFrei(wunsch, vorhandene) {
|
|
99
|
+
const belegt = new Set(vorhandene.map(quelleName));
|
|
100
|
+
if (!belegt.has(wunsch))
|
|
101
|
+
return wunsch;
|
|
102
|
+
for (let n = 2;; n += 1) {
|
|
103
|
+
const kandidat = `${wunsch}-${n}`;
|
|
104
|
+
if (!belegt.has(kandidat))
|
|
105
|
+
return kandidat;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Dieselbe Quelle mit gesetztem Anzeigenamen (E59). `undefined` lässt sie unverändert. */
|
|
109
|
+
export function quelleMitNamen(quelle, name) {
|
|
110
|
+
if (name === undefined)
|
|
111
|
+
return quelle;
|
|
112
|
+
return quelle.art === 'git' ? { art: 'git', url: quelle.url, name } : { art: 'lokal', pfad: quelle.pfad, name };
|
|
113
|
+
}
|
|
114
|
+
/** Ein Anzeigename, den schon eine andere geführte Quelle trägt (E59): Namen sind eindeutig. */
|
|
115
|
+
export class QuelleNameBelegt extends Error {
|
|
116
|
+
angefragterName;
|
|
117
|
+
andere;
|
|
118
|
+
constructor(angefragterName, andere) {
|
|
119
|
+
super(`Name „${angefragterName}" ist schon vergeben an ${quelleAlsText(andere)}. Einen anderen Namen wählen.`);
|
|
120
|
+
this.angefragterName = angefragterName;
|
|
121
|
+
this.andere = andere;
|
|
122
|
+
}
|
|
50
123
|
}
|
|
51
124
|
export class NameBelegt extends Error {
|
|
52
125
|
ziel;
|
|
@@ -73,6 +146,14 @@ export class SkillUnbekannt extends Error {
|
|
|
73
146
|
this.name = name;
|
|
74
147
|
}
|
|
75
148
|
}
|
|
149
|
+
/** Eine Quelle, die im Zustand nicht geführt wird (E54): Entfernen kennt nur bekannte Quellen. */
|
|
150
|
+
export class QuelleUnbekannt extends Error {
|
|
151
|
+
quelle;
|
|
152
|
+
constructor(quelle) {
|
|
153
|
+
super(`Quelle „${quelleAlsText(quelle)}" ist nicht bekannt`);
|
|
154
|
+
this.quelle = quelle;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
76
157
|
export class PluginMehrdeutig extends Error {
|
|
77
158
|
name;
|
|
78
159
|
quellen;
|
|
@@ -119,6 +200,36 @@ export class EinschraenkungOhneZeiger extends Error {
|
|
|
119
200
|
this.einschraenkung = einschraenkung;
|
|
120
201
|
}
|
|
121
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Schreiben in einen composer.json-Zeiger ist gesperrt, bis das Composer-Thema (E41, B13) gesondert
|
|
205
|
+
* entschieden ist (E57): der Zeiger ist ein geteiltes Team-Artefakt, ein Schreiben ohne Rücksprache
|
|
206
|
+
* beträfe Kollegen. Lesen bleibt erlaubt, nur link, unlink und plugin add/remove ohne `--local`
|
|
207
|
+
* treffen diese Sperre.
|
|
208
|
+
*/
|
|
209
|
+
export class ZeigerSchreibenGesperrt extends Error {
|
|
210
|
+
ziel;
|
|
211
|
+
constructor(ziel) {
|
|
212
|
+
super(`Schreiben in den Zeiger von ${zielSchluessel(ziel)} ist gesperrt, bis das Composer-Thema entschieden ist – ` +
|
|
213
|
+
'der Zeiger ist ein geteiltes Team-Artefakt. Nutze --local.');
|
|
214
|
+
this.ziel = ziel;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* `quelleAufraeumen` bricht ab, wenn ein Checkout der Quelle lokale Änderungen hat (E15, F31a):
|
|
219
|
+
* geprüft VOR der ersten Löschung, nichts wird gelöscht, kein stilles Teil-Überspringen. Der
|
|
220
|
+
* Aufrufer lässt bei diesem Fehler auch das Vergessen der Quelle unterbleiben, alles bleibt liegen.
|
|
221
|
+
*/
|
|
222
|
+
export class QuelleAufraeumenLokaleAenderungen extends Error {
|
|
223
|
+
checkouts;
|
|
224
|
+
constructor(checkouts) {
|
|
225
|
+
const kopf = checkouts.length === 1
|
|
226
|
+
? 'Ein Checkout hat lokale Änderungen, nichts wurde gelöscht:'
|
|
227
|
+
: `${checkouts.length} Checkouts haben lokale Änderungen, nichts wurde gelöscht:`;
|
|
228
|
+
const pfade = checkouts.map((c) => ` ${c.pfad}`).join('\n');
|
|
229
|
+
super(`${kopf}\n${pfade}\nCommitte/pushe die Änderungen oder verwirf sie, danach lässt sich das Entfernen wiederholen.`);
|
|
230
|
+
this.checkouts = checkouts;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
122
233
|
export class OrtNichtUnterstuetzt extends Error {
|
|
123
234
|
harness;
|
|
124
235
|
ort;
|
|
@@ -144,3 +255,66 @@ export class HarnessNichtVorhanden extends Error {
|
|
|
144
255
|
this.harness = harness;
|
|
145
256
|
}
|
|
146
257
|
}
|
|
258
|
+
/** Ein Name, den kein Harness-Typ trägt: Aktivieren, Deaktivieren und Säubern kennen nur bekannte Typen (E38). */
|
|
259
|
+
export class HarnessTypUnbekannt extends Error {
|
|
260
|
+
harness;
|
|
261
|
+
constructor(harness) {
|
|
262
|
+
super(`„${harness}" ist kein bekannter Harness-Typ`);
|
|
263
|
+
this.harness = harness;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Ein genannter Name, unter dem am Ziel kein fremder Eintrag liegt (E46): weder Übernehmen mit
|
|
268
|
+
* Eintrag-Auswahl noch Löschen kennen einen anderen als einen tatsächlich fremden Eintrag. Ein
|
|
269
|
+
* hmgr-eigener Link zählt hier ebenso wenig wie ein Name, der gar nicht existiert.
|
|
270
|
+
*/
|
|
271
|
+
export class FremderEintragUnbekannt extends Error {
|
|
272
|
+
ziel;
|
|
273
|
+
name;
|
|
274
|
+
constructor(ziel, name) {
|
|
275
|
+
super(`Im Ziel ${zielSchluessel(ziel)} liegt kein fremder Eintrag „${name}"`);
|
|
276
|
+
this.ziel = ziel;
|
|
277
|
+
this.name = name;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Ein Harness-Typ ohne die Fähigkeit „verwalteter Skill-Ordner" (E56): heute nur hermes. */
|
|
281
|
+
export class SkillOrdnerFaehigkeitFehlt extends Error {
|
|
282
|
+
harness;
|
|
283
|
+
constructor(harness) {
|
|
284
|
+
super(`Harness ${harness} kennt keinen verwalteten Skill-Ordner`);
|
|
285
|
+
this.harness = harness;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Für ein Ziel ist schon ein Skill-Ordner verwaltet (E56): Festlegen erlaubt nur genau einen. */
|
|
289
|
+
export class SkillOrdnerBereitsVerwaltet extends Error {
|
|
290
|
+
ziel;
|
|
291
|
+
pfad;
|
|
292
|
+
constructor(ziel, pfad) {
|
|
293
|
+
super(`Für ${zielSchluessel(ziel)} ist schon ein Skill-Ordner verwaltet: ${pfad}`);
|
|
294
|
+
this.ziel = ziel;
|
|
295
|
+
this.pfad = pfad;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Für ein Ziel ist kein Skill-Ordner verwaltet (E56): Entfernen kennt nur einen vorhandenen Eintrag. */
|
|
299
|
+
export class SkillOrdnerNichtVerwaltet extends Error {
|
|
300
|
+
ziel;
|
|
301
|
+
constructor(ziel) {
|
|
302
|
+
super(`Für ${zielSchluessel(ziel)} ist kein Skill-Ordner verwaltet`);
|
|
303
|
+
this.ziel = ziel;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Ein Pfad, der als verwalteter Skill-Ordner nicht in Frage kommt (E56): außerhalb des
|
|
308
|
+
* Hermes-Home oder Hermes' eigener Ordner `skills` (oder darunter).
|
|
309
|
+
*/
|
|
310
|
+
export class SkillOrdnerUngueltig extends Error {
|
|
311
|
+
pfad;
|
|
312
|
+
grund;
|
|
313
|
+
constructor(pfad, grund) {
|
|
314
|
+
super(grund === 'ausserhalb'
|
|
315
|
+
? `${pfad} liegt nicht unterhalb des Hermes-Home`
|
|
316
|
+
: `${pfad} ist Hermes' eigener Skill-Ordner`);
|
|
317
|
+
this.pfad = pfad;
|
|
318
|
+
this.grund = grund;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Identität einer Git-Quelle (E36): Host, Besitzer und Repo, nicht die Schreibweise der URL.
|
|
2
|
+
function ohneGitSuffix(name) {
|
|
3
|
+
return name.endsWith('.git') ? name.slice(0, -4) : name;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Die letzten beiden nicht leeren Segmente sind Besitzer und Repo. Der Host wird kleingeschrieben,
|
|
7
|
+
* Hostnamen sind case-insensitiv; der Pfadanteil bleibt, wie er geschrieben steht (E36).
|
|
8
|
+
*/
|
|
9
|
+
export function ortAusSegmenten(host, segmente) {
|
|
10
|
+
const gefiltert = segmente.filter((s) => s.length > 0);
|
|
11
|
+
const repo = ohneGitSuffix(gefiltert[gefiltert.length - 1] ?? 'repo');
|
|
12
|
+
const besitzer = gefiltert[gefiltert.length - 2] ?? '_';
|
|
13
|
+
return { host: host.toLowerCase(), besitzer, repo };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Zerlegt eine Git-URL: `git@host:besitzer/repo.git`, `ssh://git@host/besitzer/repo`,
|
|
17
|
+
* `https://host/besitzer/repo.git`, `file:///pfad`. Ein Text, der keine URL ist, gibt undefined.
|
|
18
|
+
*/
|
|
19
|
+
export function urlOrt(url) {
|
|
20
|
+
const text = url.trim().replace(/\/+$/, '');
|
|
21
|
+
if (text.includes('://')) {
|
|
22
|
+
let zerlegt;
|
|
23
|
+
try {
|
|
24
|
+
zerlegt = new URL(text);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const segmente = decodeURIComponent(zerlegt.pathname).split('/');
|
|
30
|
+
// file: hat keinen Host, alle lokalen Quellen liegen unter „local".
|
|
31
|
+
if (zerlegt.protocol === 'file:')
|
|
32
|
+
return ortAusSegmenten('local', segmente);
|
|
33
|
+
return ortAusSegmenten(zerlegt.hostname, segmente);
|
|
34
|
+
}
|
|
35
|
+
const scp = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(text);
|
|
36
|
+
if (scp)
|
|
37
|
+
return ortAusSegmenten(scp[1], scp[2].split('/'));
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export function ortSchluessel(ort) {
|
|
41
|
+
return `${ort.host}/${ort.besitzer}/${ort.repo}`;
|
|
42
|
+
}
|
package/dist/ports/harness.js
CHANGED
|
@@ -4,3 +4,16 @@ export function kannProfile(h) {
|
|
|
4
4
|
export function kannPlugins(h) {
|
|
5
5
|
return h.plugins !== undefined;
|
|
6
6
|
}
|
|
7
|
+
export function kannZurueckbauen(h) {
|
|
8
|
+
return typeof h.zurueckbauen === 'function';
|
|
9
|
+
}
|
|
10
|
+
export function kannIstRegistriert(h) {
|
|
11
|
+
return typeof h.istRegistriert === 'function';
|
|
12
|
+
}
|
|
13
|
+
export function kannSkillOrdner(h) {
|
|
14
|
+
return (typeof h.verwalteterSkillOrdner === 'function' &&
|
|
15
|
+
typeof h.skillOrdnerOrte === 'function' &&
|
|
16
|
+
typeof h.skillOrdnerFestlegen === 'function' &&
|
|
17
|
+
typeof h.skillOrdnerEntfernen === 'function' &&
|
|
18
|
+
typeof h.skillOrdnerLoeschen === 'function');
|
|
19
|
+
}
|
package/dist/ports/zustand.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hmgr",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tim Kaufmann",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"dev": "node src/cli/main.ts",
|
|
21
|
-
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"build": "tsc -p tsconfig.build.json && node scripts/copy-gui-assets.mjs",
|
|
22
|
+
"gui": "npm run build && electron dist/gui/main.js",
|
|
23
|
+
"gui:smoke": "npm run build && node scripts/gui-smoke-test.ts",
|
|
22
24
|
"test": "vitest run",
|
|
23
25
|
"test:watch": "vitest",
|
|
24
26
|
"type-check": "tsc --noEmit",
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@types/node": "^24.0.0",
|
|
34
|
+
"electron": "^44.0.0",
|
|
32
35
|
"typescript": "^5.9.0",
|
|
33
36
|
"vitest": "^3.2.0"
|
|
34
37
|
},
|