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.
@@ -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
- return quelle.art === 'git' ? quelle.url : `lokal:${quelle.pfad}`;
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
+ }
@@ -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
+ }
@@ -1,3 +1,3 @@
1
1
  export function leererZustand() {
2
- return { quellen: [], arbeitsverzeichnisse: [], bindungen: [] };
2
+ return { quellen: [], arbeitsverzeichnisse: [], bindungen: [], aktiveHarnesses: ['claude-code'], ignorierteEintraege: [] };
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.3",
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
  },