hmgr 0.0.20 → 0.0.21

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/gui/main.js CHANGED
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
13
13
  import { bauen, umgebungLesen } from "../cli/main.js";
14
14
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
15
15
  import { paketVersion } from "../adapter/paket-version.js";
16
+ import { infoPlistPfad, neuereVersionAufPlatte } from "./selbst-update.js";
16
17
  import { quelleName, quelleSchluessel } from "../kern/modell.js";
17
18
  import { kannProfile } from "../ports/harness.js";
18
19
  import { iconFuerQuelle } from "./icons.js";
@@ -331,10 +332,18 @@ function registriereIpc() {
331
332
  // Neustart-Knopf an der GUI-Update-Zeile (E80): installiert das geladene Paket sofort, statt auf
332
333
  // das nächste Beenden zu warten. Im Smoke-Test ein No-op — dort läuft `updatePruefen` ohnehin nie,
333
334
  // der Knopf bleibt verborgen, aber ein Absturz durch ein fehlendes Update darf trotzdem nie sein.
335
+ // E90: mit einem geladenen Windows-Paket `quitAndInstall`, sonst (macOS: das Bundle liegt schon
336
+ // ersetzt auf der Platte) ein Neustart des Prozesses. Entscheidend ist das Merker-Flag, nicht die
337
+ // Plattform allein.
334
338
  ipcMain.on('hmgr:neuStarten', () => {
335
339
  if (smoketest)
336
340
  return;
337
- autoUpdater.quitAndInstall();
341
+ if (geladenesPaketVorhanden) {
342
+ autoUpdater.quitAndInstall();
343
+ return;
344
+ }
345
+ app.relaunch();
346
+ app.exit(0);
338
347
  });
339
348
  ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
340
349
  ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
@@ -500,6 +509,7 @@ function fensterOeffnen() {
500
509
  });
501
510
  hauptfenster.on('closed', () => {
502
511
  hauptfenster = null;
512
+ zuletztGemeldeteGuiVersion = null;
503
513
  });
504
514
  // Der <title> der index.html setzt den Fenstertitel beim Laden sonst zurück und überschriebe die
505
515
  // Option oben (E72). Der DOM-Titel bleibt dabei, wie er ist, nur das Fenster folgt ihm nicht mehr.
@@ -1202,6 +1212,34 @@ function fensterOeffnen() {
1202
1212
  // E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
1203
1213
  // index.html), er lässt sich nur hier im Main-Prozess ablesen.
1204
1214
  .then(() => console.log(`[smoke] Fenster: ${JSON.stringify({ fenstertitel: fenster.getTitle() })}`))
1215
+ // E90: erst nach dem DOM-Auszug die Platten-Erkennung auslösen (Naht über
1216
+ // HMGR_GUI_SMOKE_PLATTENVERSION), damit die Leer-Prüfungen der Update-Zeile darin gelten.
1217
+ // Danach ein zweiter, kleiner Auszug nur zum Neustart-Modal.
1218
+ .then(async () => {
1219
+ if (process.env.HMGR_GUI_SMOKE_PLATTENVERSION === undefined)
1220
+ return;
1221
+ plattenVersionPruefen(true);
1222
+ await new Promise((r) => setTimeout(r, 800));
1223
+ const auszug = await fenster.webContents.executeJavaScript(`(async () => {
1224
+ var dlg = document.getElementById('d-neustart');
1225
+ // Ein anderer Dialog steht noch offen (Rest des ersten Auszugs): das Modal wartet
1226
+ // solange, und öffnet erst, wenn der andere zu ist.
1227
+ var anderePlatzhalter = Array.from(document.querySelectorAll('dialog[open]')).filter(function (d) { return d !== dlg; });
1228
+ var wartetSolangeAndererOffen = anderePlatzhalter.length > 0 && dlg?.open !== true;
1229
+ anderePlatzhalter.forEach(function (d) { d.close(); });
1230
+ await new Promise(function (r) { setTimeout(r, 300); });
1231
+ var offen = dlg?.open === true;
1232
+ var titel = document.getElementById('dn-titel')?.textContent ?? null;
1233
+ var zeile = document.getElementById('selbstupdate-gui');
1234
+ var zeileText = zeile && !zeile.hidden ? zeile.textContent : null;
1235
+ var neustartKnopfSichtbar = document.getElementById('selbstupdate-neustart')?.hidden === false;
1236
+ var knopfTexte = Array.from(dlg?.querySelectorAll('.dfuss button') ?? []).map(function (b) { return b.textContent; }).join('|');
1237
+ dlg?.querySelector('button[value="spaeter"]')?.click();
1238
+ var nachSpaeterOffen = dlg?.open === true;
1239
+ return JSON.stringify({ wartetSolangeAndererOffen, offen, titel, zeileText, neustartKnopfSichtbar, knopfTexte, nachSpaeterOffen });
1240
+ })()`);
1241
+ console.log(`[smoke] Neustart-Modal: ${String(auszug)}`);
1242
+ })
1205
1243
  .finally(() => app.quit());
1206
1244
  }, 3000);
1207
1245
  });
@@ -1215,7 +1253,10 @@ function fensterOeffnen() {
1215
1253
  fenster.webContents.once('did-finish-load', () => {
1216
1254
  fenster.webContents.send('hmgr:aktualisierenAnstossen', 'start');
1217
1255
  });
1218
- const intervall = setInterval(() => fenster.webContents.send('hmgr:aktualisierenAnstossen', 'intervall'), SECHS_STUNDEN_MS);
1256
+ const intervall = setInterval(() => {
1257
+ fenster.webContents.send('hmgr:aktualisierenAnstossen', 'intervall');
1258
+ plattenVersionPruefen();
1259
+ }, SECHS_STUNDEN_MS);
1219
1260
  fenster.on('closed', () => clearInterval(intervall));
1220
1261
  }
1221
1262
  // Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
@@ -1224,7 +1265,10 @@ function fensterOeffnen() {
1224
1265
  // Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
1225
1266
  if (!smoketest) {
1226
1267
  const fenster = hauptfenster;
1227
- fenster.on('focus', () => neuLadenAnstossen());
1268
+ fenster.on('focus', () => {
1269
+ neuLadenAnstossen();
1270
+ plattenVersionPruefen();
1271
+ });
1228
1272
  void beobachtetePfade().then((pfade) => {
1229
1273
  if (fenster.isDestroyed())
1230
1274
  return; // Fenster schon zu, während die Pfade ermittelt wurden
@@ -1281,7 +1325,8 @@ function updatePruefen() {
1281
1325
  .catch((fehler) => {
1282
1326
  console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1283
1327
  });
1284
- selbstUpdateMelden({ art: 'gui', version: guiVersion });
1328
+ geladenesPaketVorhanden = true;
1329
+ selbstUpdateMelden({ art: 'gui', version: guiVersion, neuStartNoetig: 'geladen' });
1285
1330
  });
1286
1331
  }
1287
1332
  void autoUpdater.checkForUpdatesAndNotify().catch((fehler) => {
@@ -1298,6 +1343,44 @@ function selbstUpdateMelden(nachricht) {
1298
1343
  return;
1299
1344
  hauptfenster.webContents.send('hmgr:selbstUpdate', nachricht);
1300
1345
  }
1346
+ /** Windows: ein Paket ist geladen und wartet auf `quitAndInstall` (E90, Merker für `hmgr:neuStarten`). */
1347
+ let geladenesPaketVorhanden = false;
1348
+ /** Die beim Start festgehaltene Version des laufenden Prozesses. */
1349
+ const laufendeVersion = paketVersion();
1350
+ const PLATTEN_PRUEFUNG_MIN_ABSTAND_MS = 30_000;
1351
+ let letztePlattenPruefung = 0;
1352
+ let zuletztGemeldeteGuiVersion = null;
1353
+ /**
1354
+ * E90, macOS-Weg: das App-Bundle wird im Hintergrund vom Deploy-Skript (ditto/rsync) ersetzt, der
1355
+ * laufende Prozess merkt das nicht. Verglichen wird die Version in `Contents/Info.plist` mit der
1356
+ * beim Start festgehaltenen. Die Info.plist statt der package.json in `app.asar`, weil Electron den
1357
+ * asar-Header je Pfad zwischenspeichert und ein ersetztes Archiv nicht neu liest. Nur gepackt und
1358
+ * nur auf macOS (nur dort gibt es ein Bundle mit Info.plist); gedrosselt auf einen Lauf je 30 s.
1359
+ * Im Smoke-Test liefert `HMGR_GUI_SMOKE_PLATTENVERSION` eine künstliche Platten-Version, sonst
1360
+ * läuft dort nie eine echte Prüfung.
1361
+ */
1362
+ function plattenVersionPruefen(erzwingen = false) {
1363
+ let platte;
1364
+ if (smoketest) {
1365
+ if (!erzwingen)
1366
+ return;
1367
+ const kuenstlich = process.env.HMGR_GUI_SMOKE_PLATTENVERSION;
1368
+ platte = kuenstlich !== undefined && kuenstlich !== '' ? kuenstlich : null;
1369
+ }
1370
+ else {
1371
+ if (!app.isPackaged || process.platform !== 'darwin')
1372
+ return;
1373
+ const jetzt = Date.now();
1374
+ if (jetzt - letztePlattenPruefung < PLATTEN_PRUEFUNG_MIN_ABSTAND_MS)
1375
+ return;
1376
+ letztePlattenPruefung = jetzt;
1377
+ platte = neuereVersionAufPlatte(infoPlistPfad(process.execPath), laufendeVersion);
1378
+ }
1379
+ if (platte === null || platte === zuletztGemeldeteGuiVersion)
1380
+ return;
1381
+ zuletztGemeldeteGuiVersion = platte;
1382
+ selbstUpdateMelden({ art: 'gui', version: platte, neuStartNoetig: 'installiert' });
1383
+ }
1301
1384
  /**
1302
1385
  * Die daneben installierte hmgr-CLI gegen npm (E77, F83=O1). Läuft auf allen Plattformen — anders
1303
1386
  * als die GUI-Prüfung hängt sie an keinem Paket-Format und an keiner Signierung. Gemeldet wird nur,
@@ -255,6 +255,21 @@
255
255
  </form>
256
256
  </dialog>
257
257
 
258
+ <!-- Neustart-Angebot (E90): ein Update für hmgr selbst liegt bereit. -->
259
+ <dialog id="d-neustart">
260
+ <form method="dialog">
261
+ <div class="dkopf">
262
+ <span class="etikett">Update</span>
263
+ <h3 id="dn-titel"></h3>
264
+ <p>hmgr startet dafür neu. Offene Dialoge gehen dabei verloren.</p>
265
+ </div>
266
+ <div class="dfuss">
267
+ <button class="knopf leise" value="spaeter">Später</button>
268
+ <button class="knopf signal" value="jetzt">Jetzt neu starten</button>
269
+ </div>
270
+ </form>
271
+ </dialog>
272
+
258
273
  <dialog id="d-erfolg">
259
274
  <form method="dialog">
260
275
  <div class="dkopf">
@@ -2668,11 +2668,39 @@
2668
2668
  die CLI installiert die GUI nicht selbst (F82). Die CLI-Zeile ist seit E82 eine Warnung, keine
2669
2669
  bloße Ansage: eine alte CLI (vor 0.0.7, kennt --version nicht) schreibt dieselben Dateien
2670
2670
  (state.json, verlauf.json) wie die neue GUI und kennt Bundles, Verlauf und WSL-Heime nicht. */
2671
+ /* Neustart-Modal (E90): je Version einmal. Nach „Später" kommt es für dieselbe Version nicht
2672
+ wieder (nur im Speicher, ein App-Neustart setzt es zurück). Steht gerade ein anderer Dialog
2673
+ offen, wartet es, bis der zu ist. */
2674
+ var neustartAngebotVersion = null;
2675
+ var neustartAbgelehnt = null;
2676
+ function neustartAnbieten(version) {
2677
+ if (version === neustartAbgelehnt) return;
2678
+ neustartAngebotVersion = version;
2679
+ neustartAngebotOeffnen();
2680
+ }
2681
+ function neustartAngebotOeffnen() {
2682
+ if (neustartAngebotVersion === null || neustartAngebotVersion === neustartAbgelehnt) return;
2683
+ if (document.querySelector('dialog[open]')) return;
2684
+ $('dn-titel').textContent = 'hmgr ' + neustartAngebotVersion + ' ist bereit';
2685
+ zeige($('d-neustart'));
2686
+ }
2687
+ $('d-neustart').addEventListener('close', function () {
2688
+ if ($('d-neustart').returnValue === 'jetzt') window.hmgr.neuStarten();
2689
+ else neustartAbgelehnt = neustartAngebotVersion;
2690
+ });
2691
+ // `close` bubbelt nicht, der Capture-Listener am Dokument sieht aber jeden geschlossenen Dialog.
2692
+ document.addEventListener('close', function (ev) {
2693
+ if (ev.target !== $('d-neustart')) setTimeout(neustartAngebotOeffnen, 0);
2694
+ }, true);
2695
+
2671
2696
  function selbstUpdateZeigen(nachricht) {
2672
2697
  if (nachricht.art === 'gui') {
2673
- $('selbstupdate-gui').textContent = 'hmgr ' + nachricht.version + ' bereit — wird beim Beenden installiert';
2698
+ $('selbstupdate-gui').textContent = nachricht.neuStartNoetig === 'installiert'
2699
+ ? 'hmgr ' + nachricht.version + ' ist installiert — Neustart lädt sie'
2700
+ : 'hmgr ' + nachricht.version + ' bereit — wird beim Beenden installiert';
2674
2701
  $('selbstupdate-gui').hidden = false;
2675
2702
  $('selbstupdate-neustart').hidden = false;
2703
+ neustartAnbieten(nachricht.version);
2676
2704
  } else if (nachricht.art === 'cli') {
2677
2705
  var p = $('selbstupdate-cli');
2678
2706
  p.textContent = '';
@@ -0,0 +1,74 @@
1
+ // Erkennung „das App-Bundle auf der Platte ist neuer als der laufende Prozess" (E90). Reine
2
+ // Funktionen ohne Electron-Import, damit sie ohne App testbar sind; das Verdrahten liegt in main.ts.
3
+ import { readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ function zerlegen(version) {
6
+ const treffer = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
7
+ if (treffer === null)
8
+ return null;
9
+ return {
10
+ kern: [Number(treffer[1]), Number(treffer[2]), Number(treffer[3])],
11
+ vorab: treffer[4] === undefined ? [] : treffer[4].split('.'),
12
+ };
13
+ }
14
+ /** Semver-Rangfolge: negativ, 0 oder positiv; Vorabversion ist kleiner als ihre Release. */
15
+ function vergleichen(a, b) {
16
+ for (let i = 0; i < 3; i++) {
17
+ const unterschied = a.kern[i] - b.kern[i];
18
+ if (unterschied !== 0)
19
+ return unterschied;
20
+ }
21
+ if (a.vorab.length === 0 && b.vorab.length === 0)
22
+ return 0;
23
+ if (a.vorab.length === 0)
24
+ return 1;
25
+ if (b.vorab.length === 0)
26
+ return -1;
27
+ for (let i = 0; i < Math.max(a.vorab.length, b.vorab.length); i++) {
28
+ const x = a.vorab[i];
29
+ const y = b.vorab[i];
30
+ if (x === undefined)
31
+ return -1;
32
+ if (y === undefined)
33
+ return 1;
34
+ const xZahl = /^\d+$/.test(x);
35
+ const yZahl = /^\d+$/.test(y);
36
+ if (xZahl && yZahl) {
37
+ if (Number(x) !== Number(y))
38
+ return Number(x) - Number(y);
39
+ }
40
+ else if (xZahl !== yZahl) {
41
+ return xZahl ? -1 : 1;
42
+ }
43
+ else if (x !== y) {
44
+ return x < y ? -1 : 1;
45
+ }
46
+ }
47
+ return 0;
48
+ }
49
+ /** `true`, wenn `platte` eine echt größere Version ist als `laufend`. Unlesbares gilt als „nein". */
50
+ export function istNeuer(platte, laufend) {
51
+ const p = zerlegen(platte);
52
+ const l = zerlegen(laufend);
53
+ return p !== null && l !== null && vergleichen(p, l) > 0;
54
+ }
55
+ /** `…/hmgr.app/Contents/MacOS/hmgr` → `…/hmgr.app/Contents/Info.plist`. */
56
+ export function infoPlistPfad(execPath) {
57
+ return path.join(path.dirname(execPath), '..', 'Info.plist');
58
+ }
59
+ /** Die CFBundleShortVersionString aus einer XML-Info.plist; `null` bei allem Unlesbaren. */
60
+ export function plistVersion(plistPfad) {
61
+ try {
62
+ const text = readFileSync(plistPfad, 'utf8');
63
+ const treffer = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/.exec(text);
64
+ return treffer?.[1]?.trim() || null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /** Die Platten-Version, wenn sie neuer ist als die laufende, sonst `null`. Wirft nie. */
71
+ export function neuereVersionAufPlatte(plistPfad, laufend) {
72
+ const platte = plistVersion(plistPfad);
73
+ return platte !== null && istNeuer(platte, laufend) ? platte : null;
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",