hmgr 0.0.39 → 0.0.42

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.
@@ -0,0 +1,31 @@
1
+ // Die Änderungsliste als mitgelieferte JSON-Datei dist/aenderungen.json (E108). Der Adapter liegt in
2
+ // dist/adapter/, die Datei eine Ebene höher; im Quellbaum (node src/cli/main.ts) gibt es sie nicht,
3
+ // dann ist die Liste leer. Fehlende oder kaputte Datei ist nie ein Fehler.
4
+ import { readFile } from 'node:fs/promises';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { ohneBom } from "./datei-text.js";
7
+ export class AenderungenJson {
8
+ pfad;
9
+ constructor(pfad = fileURLToPath(new URL('../aenderungen.json', import.meta.url))) {
10
+ this.pfad = pfad;
11
+ }
12
+ async lesen() {
13
+ const roh = await readFile(this.pfad, 'utf8').catch(() => undefined);
14
+ if (roh === undefined)
15
+ return {};
16
+ try {
17
+ const daten = JSON.parse(ohneBom(roh));
18
+ if (typeof daten.versionen !== 'object' || daten.versionen === null || Array.isArray(daten.versionen))
19
+ return {};
20
+ const ergebnis = {};
21
+ for (const [version, commits] of Object.entries(daten.versionen)) {
22
+ if (Array.isArray(commits))
23
+ ergebnis[version] = commits.filter((c) => typeof c === 'string');
24
+ }
25
+ return ergebnis;
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "fassung": 1,
3
+ "versionen": {
4
+ "0.0.42": [
5
+ "Der Verlauf meldet jetzt auch neue und entfernte Skills einer Quelle, nicht nur geänderte.",
6
+ "Verlauf und „hmgr update“ sind leichter zu lesen: Quellen erscheinen mit ihrem Namen, darunter Neu, Entfernt und Geändert, ohne Commit-Kürzel. Quellen ohne Änderung an deinen Skills stehen gesammelt in einer Zeile am Ende."
7
+ ],
8
+ "0.0.41": [
9
+ "Im Verlauf steht unter „hmgr … installiert\" jetzt aufklappbar, was sich in der neuen Version geändert hat."
10
+ ]
11
+ }
12
+ }
package/dist/cli/hilfe.js CHANGED
@@ -496,10 +496,16 @@ damit nichts verloren geht. Lokale Quellen haben keinen Checkout und bleiben unb
496
496
  Quelle ohne eigenen Checkout wird dabei aufgefrischt (E85): ihr Katalog zeigt danach, was neu im
497
497
  Remote liegt, auch ohne dass irgendwo ein Checkout von ihr existiert.
498
498
 
499
- Unter jeder „aktualisiert"-Zeile steht, welche Skills der neue Stand geändert hat (E70): die Namen
500
- kommen aus dem Vergleich von altem und neuem Commit, gezählt werden nur Skills, die in diesem
501
- Checkout auch ausgecheckt sind. Berührt ein Commit nur Dateien daneben (README, CI), bleibt die
502
- Zeile weg.
499
+ Aktualisierte Checkouts erscheinen je Quelle als Gruppe (E109): der Name der Quelle aus
500
+ hmgr source list, darunter „Neu", „Entfernt" und „Geändert" mit den Skill-Namen, leere Zeilen
501
+ fallen weg. Neu und entfernt gelten für alle Skills der Quelle (der Vergleich von altem und neuem
502
+ Commit nach dem Namen aus der SKILL.md, eine Umbenennung zeigt beides), „Geändert" nur für Skills,
503
+ die in diesem Checkout ausgecheckt sind (E70). Liegt der Checkout auf einem anderen Ref als main
504
+ oder master, steht er in Klammern hinter dem Namen, etwa „cmux (nightly)". Quellen, bei denen sich
505
+ an Skills nichts getan hat (README, CI), nennt eine Zeile am Ende des Berichts: „Außerdem neuer
506
+ Stand bei cmux, ohne Änderung an deinen Skills." Commit-Kürzel zeigt update nicht mehr, sie stehen
507
+ in ~/.hmgr/verlauf.json. Ist eine Quelle nicht mehr in der Quellenliste, steht ihr Pfad
508
+ besitzer/repo statt des Namens.
503
509
 
504
510
  Im selben Lauf gleicht update die installierten Bundles gegen ihre Definitionen ab (E69): eine
505
511
  Definition, die inzwischen ein Mitglied mehr oder weniger nennt, wird in jedem Ziel nachgezogen, in
@@ -557,12 +563,22 @@ die hmgr update schreibt und die GUI bei jedem automatischen und angestoßenen L
557
563
  liest. Ein Lauf ohne Ereignis (nichts Neues) steht nicht drin, die Datei ist ein Changelog, keine
558
564
  vollständige Historie — sie hält die letzten 20 Einträge.
559
565
 
560
- Je Eintrag steht eine Kopfzeile mit Datum und Uhrzeit, darunter je Ereignis eine Zeile: ein
561
- aktualisierter Checkout mit Quelle, Ref und Commit-Stand samt geänderten Skills (E70), ein
562
- nachgezogenes Bundle mit gelegten und entfernten Links, ein aufgeräumter (gelöschter) Checkout, ein
563
- Fehler zu einer Quelle oder einem Bundle, dazu hmgr-Ereignisse: ein Versionssprung von hmgr selbst
564
- (GUI oder CLI, erkannt beim Start bzw. bei jedem CLI-Aufruf) oder ein gefundenes Update, das noch
565
- nicht installiert ist. Plugins stehen nicht im Verlauf (siehe hmgr help update).
566
+ Je Eintrag steht eine Kopfzeile mit Datum und Uhrzeit („Heute, 00:26", „Gestern, 22:58", sonst
567
+ „06.10.2026, 17:42"), darunter die Ereignisse. Aktualisierte Checkouts stehen je Quelle als Gruppe
568
+ (E109): der Name der Quelle, darunter „Neu", „Entfernt" und „Geändert" mit Skill-Namen, leere
569
+ Zeilen fallen weg; ein Ref außer main oder master steht in Klammern hinter dem Namen. Neu und
570
+ entfernt gelten für alle Skills der Quelle (eine Umbenennung zeigt beides), „Geändert" nur für
571
+ ausgecheckte Skills (E70). Quellen ohne Skill-Änderung sammelt eine Zeile am Ende des Laufs
572
+ („Außerdem neuer Stand bei cmux, ohne Änderung an deinen Skills."). Dazwischen die übrigen
573
+ Ereignisse, je eine Zeile: ein nachgezogenes Bundle mit gelegten und entfernten Links, ein
574
+ aufgeräumter (gelöschter) Checkout, ein Fehler zu einer Quelle oder einem Bundle, dazu
575
+ hmgr-Ereignisse: ein Versionssprung von hmgr selbst (GUI oder CLI, erkannt beim Start bzw. bei
576
+ jedem CLI-Aufruf) oder ein gefundenes Update, das noch nicht installiert ist. Einträge aus der
577
+ Zeit vor E109 zeigen nur „Geändert". Plugins stehen nicht im Verlauf (siehe hmgr help update).
578
+
579
+ Unter einem „installiert"-Eintrag von hmgr stehen die Änderungen: je Version seit der zuvor
580
+ vermerkten die Nummer und darunter die Änderungen als kurze Sätze (E108). Einträge ohne bekannte Vorversion
581
+ zeigen nur die eigene Version, ohne mitgelieferte Änderungsliste entfällt der Abschnitt.
566
582
 
567
583
  Der Verlauf ist je Rechner: mehrere Maschinen führen ihre eigene Datei, es gibt kein Zusammenführen.
568
584
 
package/dist/cli/main.js CHANGED
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { realpathSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
+ import { checkoutGruppen, quellenAnzeigeName, sammelZeile, verlaufZeitpunkt } from "./verlauf-anzeige.js";
7
8
  import { Anwendungsfaelle, pluginWeltText, } from "../kern/anwendungsfaelle.js";
8
9
  import { AbhaengigkeitFehlt, BundleDateiFehler, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, ClaudeCliNichtErreichbar, HeimUnbekannt, KeinPassenderRef, LinkAnlegenFehlgeschlagen, McpAbweichenderEintrag, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpKatalogFehler, McpNichtInGeteilterDatei, McpNurGeteilterFundort, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUebernahmeUnvollstaendig, McpUnbekannt, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, WslBrueckeFehlgeschlagen, ZeigerSchreibenGesperrt, heimAdresseAlsText, heimAdresseZerlegen, wslDistroVonOrt, wslUncZerlegen, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
9
10
  import { Agents } from "../adapter/agents.js";
@@ -22,6 +23,7 @@ import { TerminalRueckfrage } from "../adapter/terminal.js";
22
23
  import { ProzessUmgebung } from "../adapter/umgebung.js";
23
24
  import { ZustandJson } from "../adapter/zustand-json.js";
24
25
  import { VerlaufJson } from "../adapter/verlauf-json.js";
26
+ import { AenderungenJson } from "../adapter/aenderungen-json.js";
25
27
  import { HILFE, hilfeText } from "./hilfe.js";
26
28
  export function umgebungLesen(env = process.env) {
27
29
  const home = env.HOME ?? homedir();
@@ -104,6 +106,7 @@ export function bauen(umgebung, optionen = {}) {
104
106
  variablen: async (distro) => await new WslBruecke(distro, wslUmgebungAusfuehren).variablen(),
105
107
  },
106
108
  verlauf,
109
+ aenderungen: optionen.aenderungen ?? new AenderungenJson(),
107
110
  };
108
111
  return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
109
112
  }
@@ -762,16 +765,27 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
762
765
  const { checkouts: liste, pluginFehler, bundles, aufraeumen, doctor, } = await anwendungsfaelle.aktualisieren({ ausloeser: 'cli', ...(nurQuelle === undefined ? {} : { quelle: nurQuelle }) });
763
766
  if (liste.length === 0)
764
767
  console.log('Keine Checkouts.');
765
- for (const { checkout, aktualisierung, geaenderteSkills } of liste) {
766
- const kopf = `${quelleAlsText(checkout.quelle)} @ ${checkout.ref}`;
767
- if (aktualisierung.ergebnis === 'aktualisiert') {
768
- console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
769
- // Auf Skill-Ebene berichten (E70): sonst steht da nur, dass die Quelle neue Commits hat.
770
- // Ohne Treffer bleibt die Zeile weg — der Commit hat dann nichts in einem Skill geändert.
771
- if (geaenderteSkills.length > 0)
772
- console.log(` ${zahlwort(geaenderteSkills.length, 'Skill', 'Skills')} geändert: ${geaenderteSkills.join(', ')}`);
773
- }
774
- else if (aktualisierung.ergebnis === 'unveraendert')
768
+ const updateQuellen = liste.length === 0 ? [] : await anwendungsfaelle.quellen();
769
+ // Aktualisierte Checkouts als Quellengruppen, ohne Commit-Kürzel (E109, F125); Quellen ohne
770
+ // Skill-Änderung sammelt die Zeile am Ende. Die übrigen Zeilenarten nennen die Quelle beim Namen.
771
+ const { zeilen: updateGruppen, ohneAenderung: updateOhne } = checkoutGruppen(liste
772
+ .filter(({ aktualisierung }) => aktualisierung.ergebnis === 'aktualisiert')
773
+ .map(({ checkout, geaenderteSkills, neueSkills, entfernteSkills }) => ({
774
+ quelle: quelleAlsText(checkout.quelle),
775
+ ref: checkout.ref,
776
+ neu: neueSkills,
777
+ entfernt: entfernteSkills,
778
+ geaendert: geaenderteSkills,
779
+ })), updateQuellen);
780
+ for (const zeile of updateGruppen)
781
+ console.log(zeile);
782
+ let updateAndere = updateGruppen.length > 0;
783
+ for (const { checkout, aktualisierung } of liste) {
784
+ if (aktualisierung.ergebnis === 'aktualisiert')
785
+ continue;
786
+ updateAndere = true;
787
+ const kopf = quellenAnzeigeName({ quelle: quelleAlsText(checkout.quelle), ref: checkout.ref }, updateQuellen);
788
+ if (aktualisierung.ergebnis === 'unveraendert')
775
789
  console.log(` unverändert ${kopf}`);
776
790
  else if (aktualisierung.ergebnis === 'uebersprungen')
777
791
  console.log(` übersprungen ${kopf} lokale Änderung (E15)`);
@@ -805,6 +819,14 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
805
819
  else
806
820
  console.log(` liegen lassen Arbeitsverzeichnis ${pfad} (nicht mehr auf der Platte, ohne Bestätigung nicht vergessen)`);
807
821
  }
822
+ // Die Sammelzeile steht ganz am Ende des Berichts (E109); „Außerdem" nur, wenn sonst etwas im Lauf stand.
823
+ const updateSammel = sammelZeile(updateOhne, !updateAndere &&
824
+ pluginFehler.length === 0 &&
825
+ bundleBericht(bundles).length === 0 &&
826
+ aufraeumen.verwaiste.length === 0 &&
827
+ doctor.fehlendeArbeitsverzeichnisse.length === 0);
828
+ if (updateSammel !== undefined)
829
+ console.log(updateSammel);
808
830
  // Zuletzt hmgr selbst (E77): wer schon aktualisiert, will auch wissen, dass sein hmgr alt ist.
809
831
  // Verglichen wird die eigene Version (E72) gegen npm, gemeldet wird nur — installiert wird
810
832
  // nichts. Offline oder Registry unerreichbar heißt keine Zeile, still: der Lauf selbst ist
@@ -856,10 +878,39 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
856
878
  const eintraege = await anwendungsfaelle.verlauf();
857
879
  if (eintraege.length === 0)
858
880
  console.log('Noch keine Ereignisse.');
881
+ const quellenListe = eintraege.length === 0 ? [] : await anwendungsfaelle.quellen();
859
882
  for (const eintrag of eintraege) {
860
883
  console.log(verlaufZeitpunkt(eintrag.zeit));
884
+ // Erst die Quellengruppen, dann die übrigen Ereignisse, zuletzt die Sammelzeile (E109).
885
+ const vermerke = [];
861
886
  for (const ereignis of eintrag.ereignisse)
887
+ if (ereignis.art === 'checkout')
888
+ vermerke.push({
889
+ quelle: ereignis.quelle,
890
+ ref: ereignis.ref,
891
+ neu: ereignis.neu ?? [],
892
+ entfernt: ereignis.entfernt ?? [],
893
+ geaendert: ereignis.skills,
894
+ });
895
+ const { zeilen: gruppenZeilen, ohneAenderung } = checkoutGruppen(vermerke, quellenListe);
896
+ for (const zeile of gruppenZeilen)
897
+ console.log(zeile);
898
+ let andere = gruppenZeilen.length > 0;
899
+ for (const ereignis of eintrag.ereignisse) {
900
+ if (ereignis.art === 'checkout')
901
+ continue;
902
+ andere = true;
862
903
  console.log(` ${verlaufEreignisZeile(ereignis)}`);
904
+ // Unter einem Versionssprung die Commits der neuen Version(en), eingerückt (E108).
905
+ if (ereignis.art === 'hmgr-installiert') {
906
+ const aenderungen = await anwendungsfaelle.hmgrAenderungen(ereignis.vorher, ereignis.version);
907
+ for (const zeile of aenderungsZeilen(aenderungen))
908
+ console.log(zeile);
909
+ }
910
+ }
911
+ const sammel = sammelZeile(ohneAenderung, !andere);
912
+ if (sammel !== undefined)
913
+ console.log(sammel);
863
914
  }
864
915
  return 0;
865
916
  }
@@ -1084,20 +1135,12 @@ function bundleBericht(eintraege) {
1084
1135
  }
1085
1136
  return zeilen;
1086
1137
  }
1087
- /** Kopfzeile eines Verlaufseintrags (E80): lokale Zeit, damit sie zum Rechner des Lesers passt. */
1088
- function verlaufZeitpunkt(iso) {
1089
- const datum = new Date(iso);
1090
- const zwei = (n) => String(n).padStart(2, '0');
1091
- return (`${zwei(datum.getDate())}.${zwei(datum.getMonth() + 1)}.${datum.getFullYear()} ` +
1092
- `${zwei(datum.getHours())}:${zwei(datum.getMinutes())}`);
1138
+ /** Je Version die Nummer und darunter ihre Commit-Betreffs, tiefer eingerückt als die Ereigniszeile (E108). */
1139
+ function aenderungsZeilen(aenderungen) {
1140
+ return aenderungen.flatMap((a) => [` ${a.version}`, ...a.commits.map((c) => ` ${c}`)]);
1093
1141
  }
1094
- /** Eine Ereigniszeile im Verlauf (E80), eingerückt unter der Kopfzeile ihres Laufs. */
1142
+ /** Eine Ereigniszeile im Verlauf (E80), eingerückt unter der Kopfzeile ihres Laufs. Checkouts stellt `checkoutGruppen` dar (E109). */
1095
1143
  function verlaufEreignisZeile(ereignis) {
1096
- if (ereignis.art === 'checkout') {
1097
- const stand = `${ereignis.von.slice(0, 7)} → ${ereignis.nach.slice(0, 7)}`;
1098
- const skills = ereignis.skills.length > 0 ? `, Skills: ${ereignis.skills.join(', ')}` : '';
1099
- return `${ereignis.quelle} @ ${ereignis.ref}: ${stand}${skills}`;
1100
- }
1101
1144
  if (ereignis.art === 'bundle') {
1102
1145
  const teile = [
1103
1146
  ereignis.gelegt.length > 0 ? `gelegt ${ereignis.gelegt.join(', ')}` : undefined,
@@ -0,0 +1,61 @@
1
+ // Darstellung von Verlauf und `hmgr update` für Menschen (E109): Quellengruppen mit Neu, Entfernt,
2
+ // Geändert und eine Sammelzeile für Quellen ohne Skill-Änderung. Reine Funktionen, die GUI baut
3
+ // dieselbe Darstellung im Renderer nach (renderer.js, verlaufGruppen).
4
+ import { quelleVerlaufName } from "../kern/modell.js";
5
+ /** Name der Quelle, bei einem Ref außer main/master mit dem Ref in Klammern dahinter. */
6
+ export function quellenAnzeigeName(vermerk, quellen) {
7
+ const name = quelleVerlaufName(vermerk.quelle, quellen);
8
+ return vermerk.ref === 'main' || vermerk.ref === 'master' ? name : `${name} (${vermerk.ref})`;
9
+ }
10
+ const BESCHRIFTUNG_BREITE = 11;
11
+ /**
12
+ * Je Quelle mit Skill-Änderung eine Gruppe (Name, darunter Neu/Entfernt/Geändert, leere fallen weg),
13
+ * eingerückt für die Ausgabe unter einer Kopfzeile. `ohneAenderung` sind die Namen der übrigen.
14
+ */
15
+ export function checkoutGruppen(vermerke, quellen) {
16
+ const zeilen = [];
17
+ const ohneAenderung = [];
18
+ for (const v of vermerke) {
19
+ const name = quellenAnzeigeName(v, quellen);
20
+ const teile = [
21
+ ['Neu', v.neu],
22
+ ['Entfernt', v.entfernt],
23
+ ['Geändert', v.geaendert],
24
+ ];
25
+ const belegt = teile.filter(([, namen]) => namen.length > 0);
26
+ if (belegt.length === 0) {
27
+ if (!ohneAenderung.includes(name))
28
+ ohneAenderung.push(name);
29
+ continue;
30
+ }
31
+ zeilen.push(` ${name}`);
32
+ for (const [beschriftung, namen] of belegt)
33
+ zeilen.push(` ${beschriftung.padEnd(BESCHRIFTUNG_BREITE)}${namen.join(', ')}`);
34
+ }
35
+ return { zeilen, ohneAenderung };
36
+ }
37
+ /** Die Sammelzeile am Ende; `alleinig`, wenn sonst nichts im Lauf steht. */
38
+ export function sammelZeile(namen, alleinig) {
39
+ if (namen.length === 0)
40
+ return undefined;
41
+ const liste = namen.join(', ');
42
+ return alleinig
43
+ ? ` Neuer Stand bei ${liste}, ohne Änderung an deinen Skills.`
44
+ : ` Außerdem neuer Stand bei ${liste}, ohne Änderung an deinen Skills.`;
45
+ }
46
+ /**
47
+ * Kopfzeile eines Verlaufseintrags (E80, E109): „Heute, 00:26", „Gestern, 22:58", sonst
48
+ * „06.10.2026, 17:42". Lokale Zeit, damit sie zum Rechner des Lesers passt.
49
+ */
50
+ export function verlaufZeitpunkt(iso, jetzt = new Date()) {
51
+ const datum = new Date(iso);
52
+ const zwei = (n) => String(n).padStart(2, '0');
53
+ const uhr = `${zwei(datum.getHours())}:${zwei(datum.getMinutes())}`;
54
+ const tag = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
55
+ const gestern = new Date(jetzt.getFullYear(), jetzt.getMonth(), jetzt.getDate() - 1).getTime();
56
+ if (tag(datum) === tag(jetzt))
57
+ return `Heute, ${uhr}`;
58
+ if (tag(datum) === gestern)
59
+ return `Gestern, ${uhr}`;
60
+ return `${zwei(datum.getDate())}.${zwei(datum.getMonth() + 1)}.${datum.getFullYear()}, ${uhr}`;
61
+ }
package/dist/gui/main.js CHANGED
@@ -11,6 +11,7 @@ import { randomUUID } from 'node:crypto';
11
11
  import { stat } from 'node:fs/promises';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { bauen, umgebungLesen } from "../cli/main.js";
14
+ import { AenderungenJson } from "../adapter/aenderungen-json.js";
14
15
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
15
16
  import { paketVersion } from "../adapter/paket-version.js";
16
17
  import { infoPlistPfad, MeldungsPuffer, neuereVersionAufPlatte } from "./selbst-update.js";
@@ -129,6 +130,11 @@ const rueckfrage = new GuiRueckfrage();
129
130
  const { anwendungsfaelle, ports } = bauen(umgebung, {
130
131
  rueckfrage,
131
132
  ...(smoketest ? { claudeAusfuehrer: smoketestClaudeAusfuehrer } : {}),
133
+ // Nur der Smoke-Test biegt die Änderungsliste auf eine Fixture-Datei um (E108); sonst die
134
+ // mitgelieferte dist/aenderungen.json.
135
+ ...(smoketest && process.env.HMGR_GUI_SMOKE_AENDERUNGEN !== undefined
136
+ ? { aenderungen: new AenderungenJson(process.env.HMGR_GUI_SMOKE_AENDERUNGEN) }
137
+ : {}),
132
138
  });
133
139
  /**
134
140
  * Wer den laufenden Aktualisieren-Lauf ausgelöst hat (E80): der Renderer reicht es bei jedem Aufruf
@@ -269,6 +275,8 @@ function registriereIpc() {
269
275
  art: quelle.art,
270
276
  text: quelleAlsText(quelle),
271
277
  name: quelleName(quelle),
278
+ // Nur der gespeicherte Name (E109): der Verlauf braucht den Unterschied zum abgeleiteten.
279
+ gespeichert: quelle.name,
272
280
  }))));
273
281
  // Quelle-Verwaltung in den Einstellungen (E54, E59): Text-zu-Quelle passiert im Main-Prozess, der
274
282
  // Renderer kennt nur Zeichenketten. Der Anzeigename kommt serverseitig mit, damit dem Renderer nie
@@ -280,6 +288,7 @@ function registriereIpc() {
280
288
  art: ergebnis.quelle.art,
281
289
  text: quelleAlsText(ergebnis.quelle),
282
290
  name: quelleName(ergebnis.quelle),
291
+ gespeichert: ergebnis.quelle.name,
283
292
  },
284
293
  skills: ergebnis.skills,
285
294
  marketplace: ergebnis.marketplace,
@@ -289,7 +298,7 @@ function registriereIpc() {
289
298
  ipcMain.handle('hmgr:quelleEntfernen', async (_event, text) => auffangen(async () => anwendungsfaelle.quelleEntfernen(quelleAusText(text))));
290
299
  ipcMain.handle('hmgr:quelleUmbenennen', async (_event, text, name) => auffangen(async () => {
291
300
  const umbenannt = await anwendungsfaelle.quelleUmbenennen(quelleAusText(text), name);
292
- return { art: umbenannt.art, text: quelleAlsText(umbenannt), name: quelleName(umbenannt) };
301
+ return { art: umbenannt.art, text: quelleAlsText(umbenannt), name: quelleName(umbenannt), gespeichert: umbenannt.name };
293
302
  }));
294
303
  // Aufräum-Angebot nach dem Entfernen einer Quelle (E58): Inventar vor dem eigentlichen Entfernen
295
304
  // erheben (der Renderer ruft das, bevor er quelleEntfernen aufruft), dann bei Zustimmung abräumen.
@@ -329,6 +338,8 @@ function registriereIpc() {
329
338
  }));
330
339
  // Changelog der letzten Läufe (E80), neueste zuerst.
331
340
  ipcMain.handle('hmgr:verlauf', async () => auffangen(async () => anwendungsfaelle.verlauf()));
341
+ // Änderungen (Commit-Betreffs) der Versionen eines hmgr-Versionssprungs (E108).
342
+ ipcMain.handle('hmgr:hmgrAenderungen', async (_event, vorher, version) => auffangen(async () => anwendungsfaelle.hmgrAenderungen(vorher, version)));
332
343
  // E77: der Prüfen-Knopf stößt zusätzlich zum Quellen-Lauf die Prüfung auf hmgr selbst an. Kein
333
344
  // `handle`, sondern `send`/`on`: der Renderer wartet auf nichts, das Ergebnis kommt später von
334
345
  // sich aus über 'hmgr:selbstUpdate'.
@@ -712,6 +723,23 @@ function fensterOeffnen() {
712
723
  verlaufBloecke = document.querySelectorAll('#verlauf .block').length;
713
724
  if (verlaufBloecke > 0) break;
714
725
  }
726
+ // E108: die Änderungslisten kommen nach dem Verlauf per IPC; warten, bis jeder Platz
727
+ // entweder sein details hat oder (ohne Daten) entfernt ist.
728
+ for (var wartezeit2 = 0; wartezeit2 < 2000; wartezeit2 += 100) {
729
+ if (document.querySelector('#verlauf .aenderungen-platz:empty') === null) break;
730
+ await new Promise((r) => setTimeout(r, 100));
731
+ }
732
+ var aenderungenDetails = document.querySelectorAll('#verlauf details.aenderungen').length;
733
+ var aenderungenElement = document.querySelector('#verlauf details.aenderungen');
734
+ var aenderungenSummary = aenderungenElement?.querySelector('summary')?.textContent?.trim() ?? null;
735
+ var aenderungenOffenVorher = aenderungenElement?.open ?? null;
736
+ aenderungenElement?.querySelector('summary')?.click();
737
+ var aenderungenOffenNachher = aenderungenElement?.open ?? null;
738
+ var aenderungenGruppen = aenderungenElement?.querySelectorAll('.aenderungen-version').length ?? 0;
739
+ var aenderungenText = Array.prototype.map.call(
740
+ aenderungenElement?.querySelectorAll('.aenderungen-version') ?? [],
741
+ (g) => g.textContent.replace(/\\s+/g, ' ').trim(),
742
+ ).join(' ');
715
743
  var verlaufText = document.getElementById('verlauf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
716
744
  // Nachziehen je Quelle (E89): das Fixture hat im skills-repo einen neuen Upstream-Commit
717
745
  // (fixtureAufbauen), den erst ein Lauf für diese Quelle holt. Erst der Knopf der
@@ -1295,6 +1323,12 @@ function fensterOeffnen() {
1295
1323
  verlaufVorhanden,
1296
1324
  verlaufText,
1297
1325
  verlaufBloecke,
1326
+ aenderungenDetails,
1327
+ aenderungenSummary,
1328
+ aenderungenOffenVorher,
1329
+ aenderungenOffenNachher,
1330
+ aenderungenGruppen,
1331
+ aenderungenText,
1298
1332
  nachziehenFehler,
1299
1333
  nachziehenKnopfZahl,
1300
1334
  nachziehenBloeckeStart,
@@ -35,6 +35,8 @@ contextBridge.exposeInMainWorld('hmgr', {
35
35
  aktualisieren: (args) => ipcRenderer.invoke('hmgr:aktualisieren', args),
36
36
  /** Changelog der letzten Läufe (E80), neueste zuerst. */
37
37
  verlauf: () => ipcRenderer.invoke('hmgr:verlauf'),
38
+ /** Commit-Betreffs der Versionen zwischen `vorher` (exklusiv) und `version` (E108), neueste zuerst. */
39
+ hmgrAenderungen: (vorher, version) => ipcRenderer.invoke('hmgr:hmgrAenderungen', vorher, version),
38
40
  /** Installiert das geladene GUI-Update und startet neu (E80), Gegenstück zum Neustart-Knopf. */
39
41
  neuStarten: () => {
40
42
  ipcRenderer.send('hmgr:neuStarten');
@@ -2707,7 +2707,7 @@
2707
2707
  if (standLaeuft) { el.textContent = 'Prüft …'; return; }
2708
2708
  if (standNeu !== null) {
2709
2709
  el.appendChild(e('b', null, standNeu.zahlwort));
2710
- el.appendChild(document.createTextNode(' ' + standNeu.anhang + ', ' + standZeit));
2710
+ el.appendChild(document.createTextNode((standNeu.anhang === '' ? '' : ' ' + standNeu.anhang) + ', ' + standZeit));
2711
2711
  return;
2712
2712
  }
2713
2713
  var text = 'Noch nicht geprüft';
@@ -2738,6 +2738,15 @@
2738
2738
  (c.geaenderteSkills || []).forEach(function (name) { geaenderteSkills[name] = true; });
2739
2739
  });
2740
2740
  var skillZahl = Object.keys(geaenderteSkills).length;
2741
+ // Neue und entfernte Skills (E109): über alle Checkouts, doppelte Namen zählen einmal.
2742
+ var neueSkills = {};
2743
+ var entfernteSkills = {};
2744
+ ergebnis.checkouts.forEach(function (c) {
2745
+ (c.neueSkills || []).forEach(function (name) { neueSkills[name] = true; });
2746
+ (c.entfernteSkills || []).forEach(function (name) { entfernteSkills[name] = true; });
2747
+ });
2748
+ var neueZahl = Object.keys(neueSkills).length;
2749
+ var entferntZahl = Object.keys(entfernteSkills).length;
2741
2750
  // Ein nachgezogenes Bundle ist Neues wie ein neuer Commit (E69, F64=O1): sonst bliebe die
2742
2751
  // Stand-Zeile still, obwohl sich am Ziel gerade Links geändert haben.
2743
2752
  var bundlesNeu = (ergebnis.bundles || []).filter(function (b) { return b.ergebnis === 'aktualisiert'; }).length;
@@ -2748,10 +2757,15 @@
2748
2757
  var automatisch = ausloeser !== 'klick';
2749
2758
  if ((neue === 0 && bundlesNeu === 0) || !automatisch || !$('ansicht-update').hidden) {
2750
2759
  standNeu = null;
2760
+ } else if (neueZahl > 0) {
2761
+ // Ein neuer Skill ist die größte Neuigkeit (E109), danach geänderte, danach entfernte.
2762
+ standNeu = { zahlwort: neueZahl === 1 ? '1 neuer Skill' : neueZahl + ' neue Skills', anhang: '' };
2751
2763
  } else if (skillZahl > 0) {
2752
2764
  // Ist die Skill-Zahl bekannt, sagt sie mehr als die Zahl der Quellen (E70). Sonst bleibt es
2753
2765
  // beim alten Signal: eine Quelle kann Neues haben, ohne dass ein Skill davon betroffen ist.
2754
2766
  standNeu = { zahlwort: zahlwort(skillZahl, 'Skill', 'Skills'), anhang: 'aktualisiert' };
2767
+ } else if (entferntZahl > 0) {
2768
+ standNeu = { zahlwort: zahlwort(entferntZahl, 'Skill', 'Skills'), anhang: 'entfernt' };
2755
2769
  } else {
2756
2770
  standNeu = {
2757
2771
  zahlwort: neue > 0 ? zahlwort(neue, 'Quelle', 'Quellen') : zahlwort(bundlesNeu, 'Bundle', 'Bundles'),
@@ -2894,17 +2908,11 @@
2894
2908
 
2895
2909
  /**
2896
2910
  * Eine Ereigniszeile im Verlauf (E80): Marke + Text, Muster wie die frühere baueBericht-Zeile
2897
- * (Checkout/Bundle) und die CLI-Ausgabe (verlaufEreignisZeile, src/cli/main.ts).
2911
+ * und die CLI-Ausgabe (verlaufEreignisZeile, src/cli/main.ts). Checkouts stellt
2912
+ * verlaufQuellenGruppen dar (E109).
2898
2913
  */
2899
2914
  function verlaufEreignisZeile(li, ereignis) {
2900
- if (ereignis.art === 'checkout') {
2901
- li.appendChild(e('span', 'marke-ok', 'aktualisiert'));
2902
- var skills = ereignis.skills.length > 0
2903
- ? ' · ' + zahlwort(ereignis.skills.length, 'Skill', 'Skills') + ': ' + ereignis.skills.join(', ')
2904
- : '';
2905
- var stand = ' (' + ereignis.von.slice(0, 7) + ' → ' + ereignis.nach.slice(0, 7) + ')';
2906
- li.appendChild(e('span', 'pfad', ereignis.quelle + ' @ ' + ereignis.ref + skills + stand));
2907
- } else if (ereignis.art === 'bundle') {
2915
+ if (ereignis.art === 'bundle') {
2908
2916
  li.appendChild(e('span', 'marke-ok', 'Bundle'));
2909
2917
  var teile = [];
2910
2918
  if (ereignis.gelegt.length > 0) teile.push('gelegt ' + ereignis.gelegt.join(', '));
@@ -2927,6 +2935,12 @@
2927
2935
  } else if (ereignis.art === 'hmgr-installiert') {
2928
2936
  li.appendChild(e('span', 'marke-ok', 'installiert'));
2929
2937
  li.appendChild(e('span', 'pfad', (ereignis.paket === 'gui' ? 'hmgr-GUI ' : 'hmgr ') + ereignis.version));
2938
+ // E108: die Commits der Versionen seit der zuvor installierten, zum Ausklappen unter der Zeile.
2939
+ // Der Platzhalter hält die Stelle, das details kommt nach dem Aufbau des Verlaufs hinein.
2940
+ var aenderungenPlatz = e('div', 'aenderungen-platz');
2941
+ aenderungenPlatz.dataset.vorher = ereignis.vorher || '';
2942
+ aenderungenPlatz.dataset.version = ereignis.version;
2943
+ li.appendChild(aenderungenPlatz);
2930
2944
  } else if (ereignis.art === 'gui-update-geladen') {
2931
2945
  li.appendChild(e('span', 'marke-ok', 'geladen'));
2932
2946
  li.appendChild(e('span', 'pfad', 'GUI-Update ' + ereignis.version));
@@ -2936,6 +2950,89 @@
2936
2950
  }
2937
2951
  }
2938
2952
 
2953
+ /**
2954
+ * Anzeigename einer Quelle im Verlauf (E109). Baut quelleVerlaufName (src/kern/modell.ts) nach,
2955
+ * der Renderer kann die Kern-Datei nicht importieren: Quelle aus QUELLEN über die Identität
2956
+ * (Host/Besitzer/Repo, E36) suchen; fehlt sie oder hat sie keinen Namen, `besitzer/repo` aus der
2957
+ * URL, bei einer lokalen Quelle der letzte Pfadteil. Ein Ref außer main/master steht in Klammern.
2958
+ */
2959
+ function verlaufQuellenOrt(text) {
2960
+ var t = text.trim().replace(/\/+$/, '');
2961
+ var segmente = null;
2962
+ var host = '';
2963
+ var m;
2964
+ if (t.indexOf('://') >= 0) {
2965
+ try {
2966
+ var u = new URL(t);
2967
+ segmente = decodeURIComponent(u.pathname).split('/');
2968
+ host = u.protocol === 'file:' ? 'local' : u.hostname;
2969
+ } catch (x) { return null; }
2970
+ } else if (!/^[A-Za-z]:[\\/]/.test(t) && (m = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(t))) {
2971
+ host = m[1];
2972
+ segmente = m[2].split('/');
2973
+ } else {
2974
+ return null;
2975
+ }
2976
+ segmente = segmente.filter(function (x) { return x.length > 0; });
2977
+ var repo = (segmente[segmente.length - 1] || 'repo').replace(/\.git$/, '');
2978
+ return { host: host.toLowerCase(), besitzer: segmente[segmente.length - 2] || '_', repo: repo };
2979
+ }
2980
+ function verlaufQuellenName(text, ref) {
2981
+ var ort = verlaufQuellenOrt(text);
2982
+ var schluessel = ort === null ? text : ort.host + '/' + ort.besitzer + '/' + ort.repo;
2983
+ var gefunden = QUELLEN.filter(function (q) {
2984
+ var qt = q.text;
2985
+ var qo = verlaufQuellenOrt(qt);
2986
+ return (qo === null ? qt : qo.host + '/' + qo.besitzer + '/' + qo.repo) === schluessel;
2987
+ })[0];
2988
+ var name;
2989
+ if (gefunden && gefunden.gespeichert && gefunden.gespeichert.trim().length > 0) name = gefunden.gespeichert;
2990
+ else if (ort !== null && ort.besitzer !== '_') name = ort.besitzer + '/' + ort.repo;
2991
+ else if (ort !== null) name = ort.repo;
2992
+ else {
2993
+ var teile = text.replace(/[\/\\]+$/, '').split(/[\/\\]/).filter(function (x) { return x.length > 0; });
2994
+ name = teile[teile.length - 1] || text;
2995
+ }
2996
+ return ref === 'main' || ref === 'master' ? name : name + ' (' + ref + ')';
2997
+ }
2998
+
2999
+ /**
3000
+ * Die Checkout-Ereignisse eines Laufs (E109): je Quelle mit Skill-Änderung eine Gruppe (Name,
3001
+ * darunter Neu/Entfernt/Geändert, leere fallen weg), dieselbe Darstellung wie `hmgr log`
3002
+ * (src/cli/verlauf-anzeige.ts). Gibt die Namen der Quellen ohne Skill-Änderung zurück, aus
3003
+ * denen der Aufrufer die Sammelzeile baut; die Gruppen hängt sie an `ul` an.
3004
+ */
3005
+ function verlaufQuellenGruppen(ul, ereignisse) {
3006
+ var ohne = [];
3007
+ var gruppen = 0;
3008
+ ereignisse.forEach(function (ereignis) {
3009
+ if (ereignis.art !== 'checkout') return;
3010
+ var name = verlaufQuellenName(ereignis.quelle, ereignis.ref);
3011
+ var teile = [
3012
+ ['Neu', ereignis.neu || []],
3013
+ ['Entfernt', ereignis.entfernt || []],
3014
+ ['Geändert', ereignis.skills || []],
3015
+ ].filter(function (t) { return t[1].length > 0; });
3016
+ if (teile.length === 0) {
3017
+ if (ohne.indexOf(name) < 0) ohne.push(name);
3018
+ return;
3019
+ }
3020
+ gruppen += 1;
3021
+ var li = e('li', 'quellengruppe');
3022
+ li.appendChild(e('b', null, name));
3023
+ var innen = e('ul', 'gruppenzeilen');
3024
+ teile.forEach(function (t) {
3025
+ var zeile = e('li');
3026
+ zeile.appendChild(e('span', 'marke-ok', t[0]));
3027
+ zeile.appendChild(e('span', 'pfad', t[1].join(', ')));
3028
+ innen.appendChild(zeile);
3029
+ });
3030
+ li.appendChild(innen);
3031
+ ul.appendChild(li);
3032
+ });
3033
+ return { ohne: ohne, gruppen: gruppen };
3034
+ }
3035
+
2939
3036
  /**
2940
3037
  * Baut das Changelog in der Update-Ansicht aus ~/.hmgr/verlauf.json (E80): ein Block je Lauf,
2941
3038
  * neueste zuerst, das Etikett der lokale Zeitpunkt, darunter eine Zeile je Ereignis. Die Datei
@@ -2953,18 +3050,55 @@
2953
3050
  var block = e('div', 'block');
2954
3051
  var zeit = new Date(eintrag.zeit);
2955
3052
  var zwei = function (n) { return String(n).padStart(2, '0'); };
2956
- var etikett = zwei(zeit.getDate()) + '.' + zwei(zeit.getMonth() + 1) + '.' + zeit.getFullYear()
2957
- + ' ' + zwei(zeit.getHours()) + ':' + zwei(zeit.getMinutes());
3053
+ // „Heute, 00:26", „Gestern, 22:58", sonst „06.10.2026, 17:42" (E109), lokale Zeit.
3054
+ var uhr = zwei(zeit.getHours()) + ':' + zwei(zeit.getMinutes());
3055
+ var jetzt = new Date();
3056
+ var tagStart = function (d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); };
3057
+ var etikett = tagStart(zeit) === tagStart(jetzt) ? 'Heute, ' + uhr
3058
+ : tagStart(zeit) === new Date(jetzt.getFullYear(), jetzt.getMonth(), jetzt.getDate() - 1).getTime() ? 'Gestern, ' + uhr
3059
+ : zwei(zeit.getDate()) + '.' + zwei(zeit.getMonth() + 1) + '.' + zeit.getFullYear() + ', ' + uhr;
2958
3060
  block.appendChild(e('span', 'etikett', etikett));
2959
3061
  var ul = e('ul');
3062
+ // Erst die Quellengruppen, dann die übrigen Ereignisse, zuletzt die Sammelzeile (E109).
3063
+ var gruppen = verlaufQuellenGruppen(ul, eintrag.ereignisse);
3064
+ var andere = gruppen.gruppen > 0;
2960
3065
  eintrag.ereignisse.forEach(function (ereignis) {
3066
+ if (ereignis.art === 'checkout') return;
3067
+ andere = true;
2961
3068
  var li = e('li');
2962
3069
  verlaufEreignisZeile(li, ereignis);
2963
3070
  ul.appendChild(li);
2964
3071
  });
3072
+ if (gruppen.ohne.length > 0) {
3073
+ ul.appendChild(e('li', 'sammelzeile', (andere ? 'Außerdem neuer' : 'Neuer') + ' Stand bei '
3074
+ + gruppen.ohne.join(', ') + ', ohne Änderung an deinen Skills.'));
3075
+ }
2965
3076
  block.appendChild(ul);
2966
3077
  box.appendChild(block);
2967
3078
  });
3079
+ // E108: die Änderungslisten nachladen, nachdem der Verlauf steht. Ein Fehler lässt nur den
3080
+ // jeweiligen Abschnitt weg, nie den Verlauf.
3081
+ var plaetze = Array.prototype.slice.call(box.querySelectorAll('.aenderungen-platz'));
3082
+ await Promise.all(plaetze.map(async function (platz) {
3083
+ var versionen = await window.hmgr.hmgrAenderungen(platz.dataset.vorher || undefined, platz.dataset.version)
3084
+ .then(function (r) { return r && r.ok ? r.daten : []; }, function () { return []; });
3085
+ if (versionen.length === 0) { platz.remove(); return; }
3086
+ var zahl = versionen.reduce(function (summe, v) { return summe + v.commits.length; }, 0);
3087
+ var d = e('details', 'notizen aenderungen');
3088
+ var s = e('summary');
3089
+ s.appendChild(keilIcon());
3090
+ s.appendChild(document.createTextNode('Änderungen (' + zahl + ')'));
3091
+ d.appendChild(s);
3092
+ versionen.forEach(function (v) {
3093
+ var gruppe = e('div', 'aenderungen-version');
3094
+ gruppe.appendChild(e('b', null, v.version));
3095
+ var liste = e('ul');
3096
+ v.commits.forEach(function (commit) { liste.appendChild(e('li', null, commit)); });
3097
+ gruppe.appendChild(liste);
3098
+ d.appendChild(gruppe);
3099
+ });
3100
+ platz.appendChild(d);
3101
+ }));
2968
3102
  }
2969
3103
 
2970
3104
  /* ================= PLUGINS ================= */
@@ -3112,13 +3246,8 @@
3112
3246
  return zeilen;
3113
3247
  }
3114
3248
 
3115
- /** Notizen sind das Langsame am Eintrag (entwurf/gui.html): zugeklappt, öffnen sich nur, wenn
3116
- jemand sie sucht. Der Wortlaut bleibt, wie er in der mcp.yaml steht, Zeilenumbrüche
3117
- eingeschlossen (weiss-raum: pre-line in styles.css). */
3118
- function mcpNotizenBlock(text) {
3119
- var d = document.createElement('details');
3120
- d.className = 'notizen';
3121
- var s = document.createElement('summary');
3249
+ /** Der Aufklapp-Keil in einer summary (dreht sich per CSS beim Öffnen). */
3250
+ function keilIcon() {
3122
3251
  var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3123
3252
  pfeil.setAttribute('viewBox', '0 0 16 16');
3124
3253
  pfeil.setAttribute('class', 'keil');
@@ -3128,7 +3257,17 @@
3128
3257
  pfeil.setAttribute('stroke-linejoin', 'round');
3129
3258
  pfeil.setAttribute('aria-hidden', 'true');
3130
3259
  pfeil.innerHTML = '<path d="M4.3 2.5 7.8 6 4.3 9.5"/>';
3131
- s.appendChild(pfeil);
3260
+ return pfeil;
3261
+ }
3262
+
3263
+ /** Notizen sind das Langsame am Eintrag (entwurf/gui.html): zugeklappt, öffnen sich nur, wenn
3264
+ jemand sie sucht. Der Wortlaut bleibt, wie er in der mcp.yaml steht, Zeilenumbrüche
3265
+ eingeschlossen (weiss-raum: pre-line in styles.css). */
3266
+ function mcpNotizenBlock(text) {
3267
+ var d = document.createElement('details');
3268
+ d.className = 'notizen';
3269
+ var s = document.createElement('summary');
3270
+ s.appendChild(keilIcon());
3132
3271
  s.appendChild(document.createTextNode('Notizen'));
3133
3272
  d.appendChild(s);
3134
3273
  d.appendChild(e('p', null, text));
@@ -490,6 +490,18 @@ img.hsymbol{border-radius:3px}
490
490
  .block .marke-ok{color:var(--emaille-hoch);font-weight:700}
491
491
  .block .marke-aus{color:var(--messing-tief);font-weight:700}
492
492
  .block .pfad{color:var(--tinte-weich)}
493
+ /* E109: je Quelle eine Gruppe mit Neu/Entfernt/Geändert, Sammelzeile für Quellen ohne Skill-Änderung. */
494
+ .block li.quellengruppe{display:block}
495
+ .block .gruppenzeilen{margin-top:5px;gap:3px;padding-left:13px;border-left:2px solid var(--strich)}
496
+ .block .gruppenzeilen li{grid-template-columns:5.5em minmax(0,1fr);gap:10px}
497
+ .block li.sammelzeile{display:block;color:var(--tinte-weich)}
498
+ /* E108: Änderungen unter „hmgr X installiert", aufklappbar wie die Notizen der MCP-Karte. */
499
+ .block .aenderungen-platz{grid-column:1 / -1}
500
+ .block .aenderungen-platz:empty{display:none}
501
+ .aenderungen{margin-top:0}
502
+ .aenderungen .aenderungen-version{margin-top:9px;padding-left:13px;border-left:2px solid var(--strich);font-size:16px;color:var(--tinte-weich)}
503
+ .block .aenderungen ul{margin-top:4px;gap:4px;font-size:16px}
504
+ .block .aenderungen li{display:block}
493
505
 
494
506
  .markt{margin-bottom:26px}
495
507
  .marktkopf{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:baseline;margin-bottom:12px;padding-bottom:10px;border-bottom:2px solid var(--tinte)}
@@ -6,6 +6,17 @@ import { passenderRef } from "./version.js";
6
6
  import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, kannZusaetzlicheHeime, } from "../ports/harness.js";
7
7
  /** So viele `plugin list`-Prozesse laufen im Ist-Zustand höchstens gleichzeitig (E93). */
8
8
  const PLUGIN_ABFRAGEN_GLEICHZEITIG = 6;
9
+ /** „0.0.39" → [0, 0, 39]; alles andere (Vorabversion, Text) → undefined. */
10
+ function versionZerlegen(version) {
11
+ const t = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
12
+ return t === null ? undefined : [Number(t[1]), Number(t[2]), Number(t[3])];
13
+ }
14
+ function versionVergleichen(a, b) {
15
+ for (let i = 0; i < 3; i++)
16
+ if (a[i] !== b[i])
17
+ return a[i] - b[i];
18
+ return 0;
19
+ }
9
20
  /**
10
21
  * Die Welt eines Orts als Text für Bericht und Verlauf (E101), in der Schreibweise von `--ort`:
11
22
  * „home" für die Installation dieses Rechners, „wsl:<distro>" für die einer Distro.
@@ -1920,10 +1931,14 @@ export class Anwendungsfaelle {
1920
1931
  // Der Quellen-Port baut Checkouts von der Platte (remote.origin.url), ohne den Zustand zu
1921
1932
  // befragen: der Anzeigename fehlt dort. Für den Bericht kommt er aus der geführten Quelle
1922
1933
  // (E59), sonst zeigte eine umbenannte Quelle hier wieder ihren abgeleiteten Namen.
1934
+ const { neu, entfernt } = await this.neueUndEntfernteSkills(checkout, aktualisierung);
1935
+ const geaendert = await this.geaenderteSkills(checkout, aktualisierung);
1923
1936
  checkouts.push({
1924
1937
  checkout: { ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) },
1925
1938
  aktualisierung,
1926
- geaenderteSkills: await this.geaenderteSkills(checkout, aktualisierung),
1939
+ geaenderteSkills: geaendert.filter((name) => !neu.includes(name)),
1940
+ neueSkills: neu,
1941
+ entfernteSkills: entfernt,
1927
1942
  });
1928
1943
  // `aktualisieren` holt für einen Checkout dieselben Refs wie `refsNachziehen` (fetch auf dem
1929
1944
  // Basis-Clone). Ist das gelaufen, braucht der Bundle-Abgleich unten die Quelle nicht ein
@@ -1992,7 +2007,7 @@ export class Anwendungsfaelle {
1992
2007
  */
1993
2008
  aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis) {
1994
2009
  const ereignisse = [];
1995
- for (const { checkout, aktualisierung, geaenderteSkills } of checkouts) {
2010
+ for (const { checkout, aktualisierung, geaenderteSkills, neueSkills, entfernteSkills } of checkouts) {
1996
2011
  if (aktualisierung.ergebnis === 'aktualisiert')
1997
2012
  ereignisse.push({
1998
2013
  art: 'checkout',
@@ -2001,6 +2016,8 @@ export class Anwendungsfaelle {
2001
2016
  von: aktualisierung.von,
2002
2017
  nach: aktualisierung.nach,
2003
2018
  skills: geaenderteSkills,
2019
+ neu: neueSkills,
2020
+ entfernt: entfernteSkills,
2004
2021
  });
2005
2022
  else if (aktualisierung.ergebnis === 'fehlgeschlagen')
2006
2023
  ereignisse.push({
@@ -2042,6 +2059,31 @@ export class Anwendungsfaelle {
2042
2059
  async verlauf() {
2043
2060
  return (await this.p.verlauf.lesen()).eintraege;
2044
2061
  }
2062
+ /**
2063
+ * Die Änderungen (Commit-Betreffs) der Versionen, die ein Sprung von `vorher` auf `version`
2064
+ * mitbrachte (E108): alle Versionen V mit vorher < V ≤ version, neueste zuerst. Ohne `vorher`
2065
+ * nur `version` selbst. Versionen ohne Eintrag in der Liste (oder ohne Commits) fehlen im
2066
+ * Ergebnis; eine nicht als x.y.z lesbare Version ergibt eine leere Liste.
2067
+ */
2068
+ async hmgrAenderungen(vorher, version) {
2069
+ const bis = versionZerlegen(version);
2070
+ if (bis === undefined)
2071
+ return [];
2072
+ const ab = vorher === undefined ? undefined : versionZerlegen(vorher);
2073
+ const liste = await this.p.aenderungen.lesen();
2074
+ const treffer = [];
2075
+ for (const [v, commits] of Object.entries(liste)) {
2076
+ const zahlen = versionZerlegen(v);
2077
+ if (zahlen === undefined || commits.length === 0)
2078
+ continue;
2079
+ const imBereich = vorher === undefined
2080
+ ? versionVergleichen(zahlen, bis) === 0
2081
+ : ab !== undefined && versionVergleichen(zahlen, ab) > 0 && versionVergleichen(zahlen, bis) <= 0;
2082
+ if (imBereich)
2083
+ treffer.push({ zahlen, eintrag: { version: v, commits } });
2084
+ }
2085
+ return treffer.sort((a, b) => versionVergleichen(b.zahlen, a.zahlen)).map((t) => t.eintrag);
2086
+ }
2045
2087
  /**
2046
2088
  * Merkt die installierte Version eines hmgr-Pakets (GUI oder CLI) und schreibt bei einem
2047
2089
  * Versionssprung einen Verlaufseintrag (E80). Der erste Aufruf ohne gemerkte Version merkt nur,
@@ -2060,7 +2102,7 @@ export class Anwendungsfaelle {
2060
2102
  const eintrag = {
2061
2103
  zeit: new Date().toISOString(),
2062
2104
  ausloeser,
2063
- ereignisse: [{ art: 'hmgr-installiert', paket, version }],
2105
+ ereignisse: [{ art: 'hmgr-installiert', paket, version, vorher: bisher }],
2064
2106
  };
2065
2107
  await this.p.verlauf.schreiben({ gemerkt, eintraege: [eintrag, ...daten.eintraege] });
2066
2108
  }
@@ -2102,6 +2144,30 @@ export class Anwendungsfaelle {
2102
2144
  namen.add(skill.name);
2103
2145
  return [...namen].sort((a, b) => a.localeCompare(b, 'de'));
2104
2146
  }
2147
+ /**
2148
+ * Neue und entfernte Skills zwischen altem und neuem Commit (E109): Vergleich der Skill-Namen
2149
+ * (E32) von `skills(quelle, von)` und `skills(quelle, nach)`, für alle Skills der Quelle. Eine
2150
+ * Umbenennung erscheint als beides. Scheitert einer der beiden Aufrufe, bleiben beide Listen
2151
+ * leer: der Bericht wird ungenauer, der Lauf nicht kaputt.
2152
+ */
2153
+ async neueUndEntfernteSkills(checkout, aktualisierung) {
2154
+ const leer = { neu: [], entfernt: [] };
2155
+ if (aktualisierung.ergebnis !== 'aktualisiert')
2156
+ return leer;
2157
+ const [vorher, nachher] = await Promise.all([
2158
+ this.p.quellen.skills(checkout.quelle, aktualisierung.von).catch(() => undefined),
2159
+ this.p.quellen.skills(checkout.quelle, aktualisierung.nach).catch(() => undefined),
2160
+ ]);
2161
+ if (vorher === undefined || nachher === undefined)
2162
+ return leer;
2163
+ const alt = new Set(vorher.map((s) => s.name));
2164
+ const neuNamen = new Set(nachher.map((s) => s.name));
2165
+ const sortiert = (namen) => [...namen].sort((a, b) => a.localeCompare(b, 'de'));
2166
+ return {
2167
+ neu: sortiert([...neuNamen].filter((n) => !alt.has(n))),
2168
+ entfernt: sortiert([...alt].filter((n) => !neuNamen.has(n))),
2169
+ };
2170
+ }
2105
2171
  /**
2106
2172
  * Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
2107
2173
  * vorhandenen Harnesses, die Arbeitsverzeichnisse aus dem Zustand. Ein Link, dessen Quelle nicht
@@ -270,6 +270,24 @@ export function quelleMitNamen(quelle, name) {
270
270
  return quelle;
271
271
  return quelle.art === 'git' ? { art: 'git', url: quelle.url, name } : { art: 'lokal', pfad: quelle.pfad, name };
272
272
  }
273
+ /**
274
+ * Anzeigename einer Quelle im Verlauf (E109), aus dem Text, den das Ereignis trägt (URL oder Pfad,
275
+ * `quelleAlsText`). Gefunden wird die Quelle in der Quellenliste über die Identität (E36), damit
276
+ * SSH- und HTTPS-Schreibweise derselben Quelle zusammenfallen; beide Quellenarten werden probiert,
277
+ * weil der Text nicht verrät, was er war. Fehlt die Quelle oder hat sie keinen Namen, steht
278
+ * `besitzer/repo` aus der URL, bei einer lokalen Quelle (oder einer URL ohne Besitzer) der letzte
279
+ * Pfadteil. GUI und CLI nutzen dieselbe Regel (der Renderer baut sie nach, renderer.js).
280
+ */
281
+ export function quelleVerlaufName(text, quellen) {
282
+ const schluessel = new Set([quelleSchluessel({ art: 'git', url: text }), quelleSchluessel({ art: 'lokal', pfad: text })]);
283
+ const gefunden = quellen.find((q) => schluessel.has(quelleSchluessel(q)));
284
+ if (gefunden?.name !== undefined && gefunden.name.trim().length > 0)
285
+ return gefunden.name;
286
+ const ort = urlOrt(text);
287
+ if (ort !== undefined && ort.besitzer !== '_')
288
+ return `${ort.besitzer}/${ort.repo}`;
289
+ return ort !== undefined ? ort.repo : letztesSegment(text);
290
+ }
273
291
  /** Ein Anzeigename, den schon eine andere geführte Quelle trägt (E59): Namen sind eindeutig. */
274
292
  export class QuelleNameBelegt extends Error {
275
293
  angefragterName;
@@ -0,0 +1,3 @@
1
+ // Änderungen: die gepflegten Sätze je hmgr-Version (E108) aus AENDERUNGEN.md, beim Bauen nach
2
+ // dist/aenderungen.json übertragen (scripts/aenderungen-erzeugen.mjs). Der Verlauf zeigt sie unter „hmgr X installiert".
3
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.39",
3
+ "version": "0.0.42",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "dev": "node src/cli/main.ts",
22
- "build": "tsc -p tsconfig.build.json && node scripts/copy-gui-assets.mjs",
22
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-gui-assets.mjs && node scripts/aenderungen-erzeugen.mjs",
23
23
  "gui": "npm run build && electron dist/gui/main.js",
24
24
  "gui:smoke": "npm run build && node scripts/gui-smoke-test.ts",
25
25
  "gui:app": "npm run build && electron-builder --mac --arm64",