hmgr 0.0.4 → 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/claude-code.js +21 -17
- package/dist/adapter/dateisystem.js +105 -5
- package/dist/adapter/git.js +96 -10
- package/dist/adapter/hermes.js +163 -10
- package/dist/adapter/skill-md.js +94 -18
- package/dist/adapter/terminal.js +1 -1
- package/dist/adapter/zustand-json.js +4 -0
- package/dist/cli/hilfe.js +129 -26
- package/dist/cli/main.js +268 -20
- 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 +688 -65
- package/dist/kern/modell.js +159 -0
- package/dist/ports/harness.js +13 -0
- package/dist/ports/zustand.js +1 -1
- package/package.json +5 -2
package/dist/cli/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
|
|
8
|
-
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, pluginSchluessel, } from "../kern/modell.js";
|
|
8
|
+
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
|
|
9
9
|
import { Agents } from "../adapter/agents.js";
|
|
10
10
|
import { ClaudeCode } from "../adapter/claude-code.js";
|
|
11
11
|
import { Hermes } from "../adapter/hermes.js";
|
|
@@ -18,7 +18,12 @@ import { ZustandJson } from "../adapter/zustand-json.js";
|
|
|
18
18
|
import { HILFE, hilfeText } from "./hilfe.js";
|
|
19
19
|
export function umgebungLesen(env = process.env) {
|
|
20
20
|
const home = env.HOME ?? homedir();
|
|
21
|
-
|
|
21
|
+
const xdgStateHome = env.XDG_STATE_HOME;
|
|
22
|
+
return {
|
|
23
|
+
home,
|
|
24
|
+
hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr'),
|
|
25
|
+
...(xdgStateHome !== undefined && xdgStateHome.length > 0 ? { xdgStateHome } : {}),
|
|
26
|
+
};
|
|
22
27
|
}
|
|
23
28
|
export function bauen(umgebung, optionen = {}) {
|
|
24
29
|
const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'));
|
|
@@ -29,7 +34,7 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
29
34
|
const ports = {
|
|
30
35
|
harnesses: [claude, new Hermes(umgebung.home), new Agents(umgebung.home)],
|
|
31
36
|
quellen: new GitQuellen(wurzel),
|
|
32
|
-
ziele: new DateisystemZiele(wurzel, zustand),
|
|
37
|
+
ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome),
|
|
33
38
|
zeiger: new ComposerZeiger(),
|
|
34
39
|
zustand,
|
|
35
40
|
rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
|
|
@@ -41,7 +46,7 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
41
46
|
* steht nirgends, weil der Hilfe-Dispatch vor jedem schalterLesen läuft.
|
|
42
47
|
*/
|
|
43
48
|
const SCHALTER = {
|
|
44
|
-
source: { werte: [], flaggen: [] },
|
|
49
|
+
source: { werte: ['name'], flaggen: [] },
|
|
45
50
|
dir: { werte: ['composer'], flaggen: [] },
|
|
46
51
|
link: {
|
|
47
52
|
werte: ['harness', 'ort', 'composer', 'quelle', 'version'],
|
|
@@ -53,6 +58,7 @@ const SCHALTER = {
|
|
|
53
58
|
sync: { werte: [], flaggen: [] },
|
|
54
59
|
update: { werte: [], flaggen: [] },
|
|
55
60
|
status: { werte: [], flaggen: [] },
|
|
61
|
+
harness: { werte: ['profil'], flaggen: [] },
|
|
56
62
|
};
|
|
57
63
|
/**
|
|
58
64
|
* Was ein einzelnes Unterkommando von seiner Familie liest, gleichlautend mit ihrer Hilfe. Ohne
|
|
@@ -60,7 +66,12 @@ const SCHALTER = {
|
|
|
60
66
|
* Schalter lesen.
|
|
61
67
|
*/
|
|
62
68
|
const UNTERKOMMANDOS = {
|
|
63
|
-
source: {
|
|
69
|
+
source: {
|
|
70
|
+
add: { woerter: 2, schalter: ['name'] },
|
|
71
|
+
remove: { woerter: 2, schalter: [] },
|
|
72
|
+
rename: { woerter: 3, schalter: [] },
|
|
73
|
+
list: { woerter: 1, schalter: [] },
|
|
74
|
+
},
|
|
64
75
|
dir: {
|
|
65
76
|
add: { woerter: 2, schalter: ['composer'] },
|
|
66
77
|
forget: { woerter: 2, schalter: [] },
|
|
@@ -71,6 +82,11 @@ const UNTERKOMMANDOS = {
|
|
|
71
82
|
remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
|
|
72
83
|
list: { woerter: 1, schalter: [] },
|
|
73
84
|
},
|
|
85
|
+
harness: {
|
|
86
|
+
list: { woerter: 1, schalter: [] },
|
|
87
|
+
enable: { woerter: 2, schalter: [] },
|
|
88
|
+
disable: { woerter: 2, schalter: [] },
|
|
89
|
+
},
|
|
74
90
|
};
|
|
75
91
|
function schalterLesen(args, kommando) {
|
|
76
92
|
const erlaubt = SCHALTER[kommando];
|
|
@@ -145,6 +161,8 @@ const UEBERNAHME_GRUND = {
|
|
|
145
161
|
'keine-url': 'Arbeitskopie ohne remote.origin.url',
|
|
146
162
|
'kein-branch': 'Arbeitskopie ohne Branch (detached HEAD)',
|
|
147
163
|
'skill-unbekannt': 'Pfad ist in der Quelle kein Skill',
|
|
164
|
+
'lokale-aenderungen': 'Ordner weicht von der Quelle ab',
|
|
165
|
+
'nicht-pruefbar': 'hmgr kann nicht prüfen, ob der Ordner der Quelle entspricht',
|
|
148
166
|
};
|
|
149
167
|
export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
150
168
|
const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
|
|
@@ -176,22 +194,24 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
176
194
|
if (kommando === 'source') {
|
|
177
195
|
const schalter = schalterLesen(argv.slice(1), 'source');
|
|
178
196
|
unterkommandoPruefen('source', schalter);
|
|
179
|
-
const rest = schalter
|
|
197
|
+
const { rest, werte } = schalter;
|
|
180
198
|
const unterkommando = rest[0];
|
|
181
199
|
if (unterkommando === 'add') {
|
|
182
200
|
const text = rest[1];
|
|
183
201
|
if (text === undefined)
|
|
184
202
|
throw new Error('source add braucht eine URL oder einen Pfad, etwa hmgr source add https://example.com/skills.git');
|
|
185
203
|
const genannt = quelleAusText(text);
|
|
186
|
-
const { quelle, skills, marketplace, bekannt } = await anwendungsfaelle.quelleHinzufuegen(genannt);
|
|
204
|
+
const { quelle, skills, marketplace, bekannt } = await anwendungsfaelle.quelleHinzufuegen(genannt, werte.get('name'));
|
|
187
205
|
// Dieselbe Quelle in anderer Schreibweise legt keine zweite an (E36).
|
|
188
206
|
if (bekannt)
|
|
189
207
|
console.log(quelleAlsText(quelle) === quelleAlsText(genannt)
|
|
190
208
|
? 'Quelle ist schon bekannt, keine zweite angelegt.'
|
|
191
209
|
: `Quelle ist schon bekannt, geführt als ${quelleAlsText(quelle)}. Keine zweite angelegt.`);
|
|
192
|
-
console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}), ${skills.length} Skills:`);
|
|
193
|
-
for (const skill of skills)
|
|
194
|
-
|
|
210
|
+
console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}) als „${quelleName(quelle)}", ${skills.length} Skills:`);
|
|
211
|
+
for (const skill of skills) {
|
|
212
|
+
const beschreibung = skill.beschreibung === undefined ? '' : ` ${kappen(skill.beschreibung)}`;
|
|
213
|
+
console.log(` ${skill.name} ${skill.pfad}${beschreibung}`);
|
|
214
|
+
}
|
|
195
215
|
if (marketplace !== null) {
|
|
196
216
|
console.log(`Marketplace ${marketplace.name}, ${marketplace.plugins.length} Plugins:`);
|
|
197
217
|
for (const plugin of marketplace.plugins) {
|
|
@@ -201,15 +221,89 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
201
221
|
}
|
|
202
222
|
return 0;
|
|
203
223
|
}
|
|
224
|
+
if (unterkommando === 'remove') {
|
|
225
|
+
const text = rest[1];
|
|
226
|
+
if (text === undefined)
|
|
227
|
+
throw new Error('source remove braucht eine URL oder einen Pfad. hmgr source list zeigt die bekannten Quellen.');
|
|
228
|
+
const genannt = quelleAusText(text);
|
|
229
|
+
// Inventar (E58) vor dem Vergessen erheben (F14): danach tragen die Links ihre Quelle nicht
|
|
230
|
+
// mehr. Bei unbekannter Quelle entfällt der Ziel-Scan, den Fehler wirft quelleEntfernen.
|
|
231
|
+
const key = quelleSchluessel(genannt);
|
|
232
|
+
// Die Rückfrage kommt jetzt VOR dem Vergessen (F31a), nennen muss sie die Quelle trotzdem
|
|
233
|
+
// unter ihrer geführten Schreibweise (E36), nicht unter der getippten.
|
|
234
|
+
const gefuehrtVorher = (await anwendungsfaelle.quellen()).find((q) => quelleSchluessel(q) === key);
|
|
235
|
+
const inventar = gefuehrtVorher === undefined ? undefined : await anwendungsfaelle.quelleAufraeumInventar(genannt);
|
|
236
|
+
const meldeVergessen = (gefuehrt, zahl) => {
|
|
237
|
+
console.log(`Quelle ${quelleAlsText(gefuehrt)} vergessen, Checkouts und Links bleiben liegen.`);
|
|
238
|
+
if (zahl === 0)
|
|
239
|
+
console.log(' Kein Link betroffen.');
|
|
240
|
+
else {
|
|
241
|
+
console.log(` ${zahl === 1 ? '1 Link gilt' : `${zahl} Links gelten`} jetzt als „Link ohne bekannte Quelle", das Aktualisieren prüft ${zahl === 1 ? 'ihn' : 'sie'} nicht mehr.`);
|
|
242
|
+
console.log(` source add derselben Quelle macht ${zahl === 1 ? 'ihn wieder zu einem normalen Link' : 'sie wieder zu normalen Links'}.`);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
// Leeres Inventar (F31a): nichts zum Aufräumen, keine Rückfrage, sofort vergessen wie bisher.
|
|
246
|
+
if (gefuehrtVorher === undefined ||
|
|
247
|
+
inventar === undefined ||
|
|
248
|
+
(inventar.links.length === 0 && inventar.checkouts.length === 0)) {
|
|
249
|
+
const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
|
|
250
|
+
meldeVergessen(gefuehrt, inventar?.links.length ?? 0);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
// Aufräum-Rückfrage VOR dem Vergessen (F31a): ein Ja mit lokalen Änderungen darf weder löschen
|
|
254
|
+
// noch die Quelle vergessen, das entscheidet erst quelleAufraeumen unten.
|
|
255
|
+
const teile = [
|
|
256
|
+
zahlwort(inventar.links.length, 'Link', 'Links'),
|
|
257
|
+
zahlwort(inventar.checkouts.length, 'Checkout', 'Checkouts'),
|
|
258
|
+
];
|
|
259
|
+
const bleibt = (n) => (n === 1 ? ', bleibt liegen' : ', bleiben liegen');
|
|
260
|
+
if (inventar.plugins.length > 0)
|
|
261
|
+
teile.push(zahlwort(inventar.plugins.length, 'installiertes Plugin', 'installierte Plugins') + bleibt(inventar.plugins.length));
|
|
262
|
+
if (inventar.zeigerPfade.length > 0)
|
|
263
|
+
teile.push(zahlwort(inventar.zeigerPfade.length, 'Zeiger-Eintrag', 'Zeiger-Einträge') + bleibt(inventar.zeigerPfade.length));
|
|
264
|
+
let frage = `Skills von ${quelleAlsText(gefuehrtVorher)} komplett vom Rechner löschen (${teile.join(', ')})?`;
|
|
265
|
+
if (inventar.checkoutsMitLokalenAenderungen.length > 0) {
|
|
266
|
+
const anzahl = inventar.checkoutsMitLokalenAenderungen.length;
|
|
267
|
+
frage +=
|
|
268
|
+
`\n${anzahl === 1 ? 'Achtung: 1 Checkout hat' : `Achtung: ${anzahl} Checkouts haben`} lokale Änderungen. ` +
|
|
269
|
+
'Ein Ja bricht dann ab, ohne zu löschen, die Quelle bleibt bekannt. Erst committen/pushen oder verwerfen, dann erneut versuchen.';
|
|
270
|
+
}
|
|
271
|
+
if (!(await ports.rueckfrage.bestaetigen(frage))) {
|
|
272
|
+
const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
|
|
273
|
+
meldeVergessen(gefuehrt, inventar.links.length);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
// Ja: erst aufräumen (wirft bei lokalen Änderungen, VOR jeder Löschung, F31a), erst danach die
|
|
277
|
+
// Quelle vergessen, damit ein Abbruch sie unberührt lässt.
|
|
278
|
+
const ergebnis = await anwendungsfaelle.quelleAufraeumen(inventar);
|
|
279
|
+
const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
|
|
280
|
+
console.log(`Quelle ${quelleAlsText(gefuehrt)} entfernt und aufgeräumt.`);
|
|
281
|
+
console.log(` entfernt ${zahlwort(ergebnis.entfernteLinks, 'Link', 'Links')}, ${zahlwort(ergebnis.entfernteCheckouts, 'Checkout', 'Checkouts')}.`);
|
|
282
|
+
if (inventar.plugins.length > 0)
|
|
283
|
+
console.log(` installierte Plugins bleiben liegen: ${inventar.plugins.join(', ')}`);
|
|
284
|
+
if (inventar.zeigerPfade.length > 0)
|
|
285
|
+
console.log(` Zeiger-Einträge bleiben liegen: ${inventar.zeigerPfade.join(', ')}`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
if (unterkommando === 'rename') {
|
|
289
|
+
const text = rest[1];
|
|
290
|
+
const name = rest[2];
|
|
291
|
+
if (text === undefined || name === undefined || name.trim().length === 0)
|
|
292
|
+
throw new Error('source rename braucht eine URL oder einen Pfad und einen neuen Namen, etwa hmgr source rename https://example.com/skills.git skills');
|
|
293
|
+
const genannt = quelleAusText(text);
|
|
294
|
+
const umbenannt = await anwendungsfaelle.quelleUmbenennen(genannt, name);
|
|
295
|
+
console.log(`Quelle ${quelleAlsText(umbenannt)} heißt jetzt „${quelleName(umbenannt)}".`);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
204
298
|
if (unterkommando === 'list') {
|
|
205
299
|
const zustand = await ports.zustand.lesen();
|
|
206
300
|
if (zustand.quellen.length === 0)
|
|
207
301
|
console.log('Keine Quellen.');
|
|
208
302
|
for (const quelle of zustand.quellen)
|
|
209
|
-
console.log(`${quelle.art} ${quelleAlsText(quelle)}`);
|
|
303
|
+
console.log(`${quelle.art} ${quelleName(quelle)} ${quelleAlsText(quelle)}`);
|
|
210
304
|
return 0;
|
|
211
305
|
}
|
|
212
|
-
throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
|
|
306
|
+
throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad>, source rename <url|pfad> <name>, source remove <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
|
|
213
307
|
}
|
|
214
308
|
if (kommando === 'dir') {
|
|
215
309
|
const schalter = schalterLesen(argv.slice(1), 'dir');
|
|
@@ -269,8 +363,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
269
363
|
await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
|
|
270
364
|
}
|
|
271
365
|
if (kommando === 'unlink') {
|
|
272
|
-
await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
|
|
366
|
+
const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
|
|
273
367
|
console.log(`Link ${skill} entfernt.`);
|
|
368
|
+
if (ergebnis.hinweis !== undefined)
|
|
369
|
+
console.log(ergebnis.hinweis);
|
|
274
370
|
return 0;
|
|
275
371
|
}
|
|
276
372
|
const quelle = werte.get('quelle');
|
|
@@ -327,6 +423,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
327
423
|
}
|
|
328
424
|
const quellenText = neueQuellen === 1 ? '1 neue Quelle' : `${neueQuellen} neue Quellen`;
|
|
329
425
|
console.log(`${uebernommen} übernommen, ${uebersprungen} übersprungen, ${quellenText}.`);
|
|
426
|
+
if (ergebnis.hinweis !== undefined)
|
|
427
|
+
console.log(ergebnis.hinweis);
|
|
330
428
|
return 0;
|
|
331
429
|
}
|
|
332
430
|
if (kommando === 'plugin') {
|
|
@@ -409,8 +507,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
409
507
|
console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
|
|
410
508
|
else if (aktualisierung.ergebnis === 'unveraendert')
|
|
411
509
|
console.log(` unverändert ${kopf}`);
|
|
412
|
-
else
|
|
510
|
+
else if (aktualisierung.ergebnis === 'uebersprungen')
|
|
413
511
|
console.log(` übersprungen ${kopf} lokale Änderung (E15)`);
|
|
512
|
+
else
|
|
513
|
+
console.log(` fehlgeschlagen ${kopf} ${aktualisierung.fehler}`);
|
|
414
514
|
}
|
|
415
515
|
for (const name of pluginHarnesses)
|
|
416
516
|
console.log(` Plugins ${name} aktualisiert (E14)`);
|
|
@@ -440,12 +540,12 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
440
540
|
if (eintrag.art === 'fremd')
|
|
441
541
|
console.log(` ${eintrag.name} fremd`);
|
|
442
542
|
else if (eintrag.art === 'link-ohne-quelle')
|
|
443
|
-
console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})`);
|
|
543
|
+
console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.name))}`);
|
|
444
544
|
else {
|
|
445
545
|
// Steht im Zeiger eine Versionseinschränkung, kommt sie vor den aufgelösten Ref (E33).
|
|
446
546
|
const einschraenkung = zielZustand.einschraenkungen.get(eintrag.link.name);
|
|
447
547
|
const spanne = einschraenkung === undefined ? '' : `${einschraenkung} → `;
|
|
448
|
-
console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})`);
|
|
548
|
+
console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.link.name))}`);
|
|
449
549
|
}
|
|
450
550
|
}
|
|
451
551
|
if (zielZustand.plugins !== undefined)
|
|
@@ -456,8 +556,132 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
456
556
|
console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
|
|
457
557
|
return 0;
|
|
458
558
|
}
|
|
559
|
+
if (kommando === 'harness') {
|
|
560
|
+
const schalter = schalterLesen(argv.slice(1), 'harness');
|
|
561
|
+
unterkommandoPruefen('harness', schalter);
|
|
562
|
+
const { rest, werte } = schalter;
|
|
563
|
+
const unterkommando = rest[0];
|
|
564
|
+
if (unterkommando === 'skills-dir') {
|
|
565
|
+
const harnessName = rest[1];
|
|
566
|
+
if (harnessName === undefined)
|
|
567
|
+
throw new Error('harness skills-dir braucht einen Harness-Namen, etwa hmgr harness skills-dir hermes. ' +
|
|
568
|
+
'hmgr harness list zeigt die bekannten Namen.');
|
|
569
|
+
const unter = rest[2];
|
|
570
|
+
const profilName = werte.get('profil');
|
|
571
|
+
const ort = profilName === undefined ? { art: 'home' } : { art: 'profil', name: profilName };
|
|
572
|
+
const ziel = { harness: harnessName, ort };
|
|
573
|
+
const ortText = beschreibeOrt(ort);
|
|
574
|
+
if (unter === undefined) {
|
|
575
|
+
restPruefen(rest, 2, 'harness', 'skills-dir');
|
|
576
|
+
const pfad = await anwendungsfaelle.verwalteterSkillOrdner(ziel);
|
|
577
|
+
if (pfad === undefined) {
|
|
578
|
+
console.log(`${harnessName} ${ortText}: kein Skill-Ordner verwaltet.`);
|
|
579
|
+
console.log(` Festlegen mit hmgr harness skills-dir ${harnessName} add <pfad>.`);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
console.log(`${harnessName} ${ortText} ${pfad}`);
|
|
583
|
+
}
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
if (unter === 'add') {
|
|
587
|
+
restPruefen(rest, 4, 'harness', 'skills-dir');
|
|
588
|
+
const pfad = rest[3];
|
|
589
|
+
if (pfad === undefined)
|
|
590
|
+
throw new Error('harness skills-dir <name> add braucht einen Pfad, etwa ' +
|
|
591
|
+
'hmgr harness skills-dir hermes add ~/.hermes/external-skills.');
|
|
592
|
+
const gesetzt = await anwendungsfaelle.skillOrdnerFestlegen(ziel, pfad);
|
|
593
|
+
console.log(`${harnessName} ${ortText} liest Skills jetzt aus ${gesetzt}.`);
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
if (unter === 'remove') {
|
|
597
|
+
restPruefen(rest, 3, 'harness', 'skills-dir');
|
|
598
|
+
const ergebnis = await anwendungsfaelle.skillOrdnerEntfernen(ziel);
|
|
599
|
+
console.log(`${harnessName} ${ortText}: Eintrag für ${ergebnis.pfad} entfernt.`);
|
|
600
|
+
const inhalt = `${zahlwort(ergebnis.hmgrLinks, 'hmgr-Link', 'hmgr-Links')}, ${zahlwort(ergebnis.fremdeEintraege, 'fremder Eintrag', 'fremde Einträge')}`;
|
|
601
|
+
const frage = `Ordner ${ergebnis.pfad} enthält noch ${inhalt}. Löschen?`;
|
|
602
|
+
if (await ports.rueckfrage.bestaetigen(frage)) {
|
|
603
|
+
await anwendungsfaelle.skillOrdnerLoeschen(ziel, ergebnis.pfad);
|
|
604
|
+
console.log(` Ordner ${ergebnis.pfad} gelöscht.`);
|
|
605
|
+
}
|
|
606
|
+
return 0;
|
|
607
|
+
}
|
|
608
|
+
throw new Error(`Unbekanntes Unterkommando „${unter}" für harness skills-dir: harness skills-dir <name>, ` +
|
|
609
|
+
'harness skills-dir <name> add <pfad> oder harness skills-dir <name> remove. ' +
|
|
610
|
+
'hmgr help harness zeigt Beispiele.');
|
|
611
|
+
}
|
|
612
|
+
if (unterkommando === 'list') {
|
|
613
|
+
const aktive = new Set(await anwendungsfaelle.aktiveHarnesses());
|
|
614
|
+
const fehlend = new Set(await anwendungsfaelle.nichtVorhandeneHarnesses());
|
|
615
|
+
for (const name of anwendungsfaelle.harnessTypen()) {
|
|
616
|
+
const aktivText = aktive.has(name) ? 'aktiv' : 'inaktiv';
|
|
617
|
+
const vorhandenText = fehlend.has(name) ? 'nicht vorhanden' : 'vorhanden';
|
|
618
|
+
console.log(`${name} ${aktivText}, ${vorhandenText}`);
|
|
619
|
+
}
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
if (unterkommando === 'enable' || unterkommando === 'disable') {
|
|
623
|
+
const name = rest[1];
|
|
624
|
+
if (name === undefined)
|
|
625
|
+
throw new Error(`harness ${unterkommando} braucht einen Harness-Namen. hmgr harness list zeigt die bekannten Harness-Typen.`);
|
|
626
|
+
const vorherAktiv = new Set(await anwendungsfaelle.aktiveHarnesses());
|
|
627
|
+
if (unterkommando === 'enable') {
|
|
628
|
+
await anwendungsfaelle.harnessAktivieren(name);
|
|
629
|
+
console.log(vorherAktiv.has(name) ? `${name} ist schon aktiv.` : `${name} aktiviert.`);
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
await anwendungsfaelle.harnessDeaktivieren(name);
|
|
633
|
+
console.log(vorherAktiv.has(name) ? `${name} deaktiviert.` : `${name} ist schon inaktiv.`);
|
|
634
|
+
// Säubern-Angebot nur, wenn eigene Konfiguration liegt, mit derselben Bedingung wie die GUI
|
|
635
|
+
// (E38, GRILL.md Entwurfskritik 4): eigene Links oder eine liegen gebliebene Registrierung
|
|
636
|
+
// ohne Link (E38 R7), sonst machten CLI und GUI verschiedene Angebote.
|
|
637
|
+
const ziele = (await anwendungsfaelle.istZustand()).filter((z) => z.ziel.harness === name &&
|
|
638
|
+
(z.eintraege.some((eintrag) => eintrag.art !== 'fremd') || z.istRegistriert));
|
|
639
|
+
if (ziele.length === 0)
|
|
640
|
+
return 0;
|
|
641
|
+
const links = ziele.reduce((n, z) => n + z.eintraege.filter((eintrag) => eintrag.art !== 'fremd').length, 0);
|
|
642
|
+
const eintraege = ziele.filter((z) => z.istRegistriert).length;
|
|
643
|
+
// Wer den harness-eigenen Eintrag angelegt hat, weiß niemand (E38 R7), also wird er benannt,
|
|
644
|
+
// nicht hmgr zugeschrieben. Gleicher Text wie bilanzText in der GUI.
|
|
645
|
+
const teile = [
|
|
646
|
+
links === 0
|
|
647
|
+
? zahlwort(ziele.length, 'leeres Ziel', 'leere Ziele')
|
|
648
|
+
: `${zahlwort(links, 'Link', 'Links')} in ${zahlwort(ziele.length, 'Ziel', 'Zielen')}`,
|
|
649
|
+
];
|
|
650
|
+
if (eintraege > 0)
|
|
651
|
+
teile.push(zahlwort(eintraege, 'harness-eigener Eintrag', 'harness-eigene Einträge'));
|
|
652
|
+
const liste = ziele.map((z) => ` ${z.verzeichnis}`).join('\n');
|
|
653
|
+
const frage = `Es liegt noch Konfiguration (${teile.join(', ')}):\n${liste}\nEntfernen?`;
|
|
654
|
+
if (!(await ports.rueckfrage.bestaetigen(frage))) {
|
|
655
|
+
console.log(' Konfiguration bleibt liegen, hmgr pflegt sie nicht mehr.');
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
658
|
+
const ergebnis = await anwendungsfaelle.saeubern(name);
|
|
659
|
+
for (const zielErgebnis of ergebnis.ziele) {
|
|
660
|
+
if (zielErgebnis.entfernteLinks.length > 0)
|
|
661
|
+
console.log(` entfernt ${beschreibeOrt(zielErgebnis.ziel.ort)} ${zielErgebnis.entfernteLinks.join(', ')}`);
|
|
662
|
+
if (zielErgebnis.konfigurationZurueckgebaut)
|
|
663
|
+
console.log(` zurückgebaut ${beschreibeOrt(zielErgebnis.ziel.ort)} harness-eigene Konfiguration`);
|
|
664
|
+
}
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
throw new Error(`${unterkommando === undefined ? 'harness braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für harness`}: harness list, harness enable <name> oder harness disable <name>. hmgr help harness zeigt Beispiele.`);
|
|
668
|
+
}
|
|
459
669
|
throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
|
|
460
670
|
}
|
|
671
|
+
/** Grund eines Links als Suffix (E37). „manuell" bleibt stumm, damit die Ausgabe ruhig bleibt. */
|
|
672
|
+
function beschreibeGrund(grund) {
|
|
673
|
+
if (grund === undefined || grund.art === 'manuell')
|
|
674
|
+
return '';
|
|
675
|
+
return grund.art === 'plugin' ? ` Plugin „${grund.plugin}"` : ' über Zeiger';
|
|
676
|
+
}
|
|
677
|
+
/** Zahl mit passender Endung, Singular oder Plural, für die Säubern-Frage bei harness disable (E38). */
|
|
678
|
+
function zahlwort(n, ein, viele) {
|
|
679
|
+
return `${n} ${n === 1 ? ein : viele}`;
|
|
680
|
+
}
|
|
681
|
+
/** Text auf `laenge` Zeichen kappen, mit „…" markiert, damit eine Skill-Liste lesbar bleibt (E39). */
|
|
682
|
+
function kappen(text, laenge = 80) {
|
|
683
|
+
return text.length <= laenge ? text : `${text.slice(0, laenge - 1)}…`;
|
|
684
|
+
}
|
|
461
685
|
function beschreibeOrt(ort) {
|
|
462
686
|
if (ort.art === 'home')
|
|
463
687
|
return 'home';
|
|
@@ -471,12 +695,21 @@ function istModellFehler(fehler) {
|
|
|
471
695
|
fehler instanceof SkillUnbekannt ||
|
|
472
696
|
fehler instanceof OrtNichtUnterstuetzt ||
|
|
473
697
|
fehler instanceof ZeigerMehrdeutig ||
|
|
698
|
+
fehler instanceof ZeigerSchreibenGesperrt ||
|
|
474
699
|
fehler instanceof HarnessNichtVorhanden ||
|
|
700
|
+
fehler instanceof HarnessTypUnbekannt ||
|
|
475
701
|
fehler instanceof KeinPassenderRef ||
|
|
476
702
|
fehler instanceof EinschraenkungOhneZeiger ||
|
|
477
703
|
fehler instanceof PluginMehrdeutig ||
|
|
478
704
|
fehler instanceof PluginUnbekannt ||
|
|
479
|
-
fehler instanceof AbhaengigkeitFehlt
|
|
705
|
+
fehler instanceof AbhaengigkeitFehlt ||
|
|
706
|
+
fehler instanceof QuelleUnbekannt ||
|
|
707
|
+
fehler instanceof QuelleNameBelegt ||
|
|
708
|
+
fehler instanceof QuelleAufraeumenLokaleAenderungen ||
|
|
709
|
+
fehler instanceof SkillOrdnerFaehigkeitFehlt ||
|
|
710
|
+
fehler instanceof SkillOrdnerBereitsVerwaltet ||
|
|
711
|
+
fehler instanceof SkillOrdnerNichtVerwaltet ||
|
|
712
|
+
fehler instanceof SkillOrdnerUngueltig);
|
|
480
713
|
}
|
|
481
714
|
/**
|
|
482
715
|
* Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
|
|
@@ -501,9 +734,24 @@ function naechsterSchritt(fehler) {
|
|
|
501
734
|
return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
|
|
502
735
|
if (fehler instanceof OrtNichtUnterstuetzt)
|
|
503
736
|
return 'profil:<name> kennt nur hermes, claude-code und agents kennen home und dir:<pfad>.';
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
737
|
+
if (fehler instanceof HarnessTypUnbekannt)
|
|
738
|
+
return 'Bekannt sind claude-code, hermes und agents. hmgr harness list zeigt sie mit ihrem Stand.';
|
|
739
|
+
if (fehler instanceof QuelleUnbekannt)
|
|
740
|
+
return 'hmgr source list zeigt die bekannten Quellen.';
|
|
741
|
+
if (fehler instanceof QuelleNameBelegt)
|
|
742
|
+
return 'hmgr source list zeigt die schon vergebenen Namen.';
|
|
743
|
+
if (fehler instanceof ZeigerSchreibenGesperrt)
|
|
744
|
+
return 'Nutze --local, das schreibt nichts in den Zeiger.';
|
|
745
|
+
if (fehler instanceof SkillOrdnerFaehigkeitFehlt)
|
|
746
|
+
return 'Nur hermes kennt einen verwalteten Skill-Ordner.';
|
|
747
|
+
if (fehler instanceof SkillOrdnerBereitsVerwaltet)
|
|
748
|
+
return `Erst entfernen: hmgr harness skills-dir ${fehler.ziel.harness} remove${fehler.ziel.ort.art === 'profil' ? ` --profil ${fehler.ziel.ort.name}` : ''}.`;
|
|
749
|
+
if (fehler instanceof SkillOrdnerNichtVerwaltet)
|
|
750
|
+
return `Festlegen mit hmgr harness skills-dir ${fehler.ziel.harness} add <pfad>${fehler.ziel.ort.art === 'profil' ? ` --profil ${fehler.ziel.ort.name}` : ''}.`;
|
|
751
|
+
if (fehler instanceof SkillOrdnerUngueltig)
|
|
752
|
+
return fehler.grund === 'ausserhalb'
|
|
753
|
+
? 'Der Pfad muss unterhalb des Hermes-Home liegen.'
|
|
754
|
+
: 'Das ist Hermes\' eigener Skill-Ordner, ein anderer Pfad muss her.';
|
|
507
755
|
return undefined;
|
|
508
756
|
}
|
|
509
757
|
export async function main(argv, umgebung) {
|
|
@@ -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
|
+
}
|