hmgr 0.0.19 → 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/cli/hilfe.js CHANGED
@@ -31,7 +31,7 @@ hmgr <kommando> [schalter]
31
31
  mcp migrate <name> MCP-Server aus der geteilten .mcp.json in den privaten Scope übernehmen
32
32
  mcp list Katalog-Einträge zeigen, mit wo sie eingehängt sind
33
33
  sync [pfad] Ziel auf den Stand seines Zeigers bringen
34
- update Alle Checkouts aktualisieren
34
+ update Alle Checkouts aktualisieren, mit <skill> oder --quelle nur eine Quelle
35
35
  status Ist-Zustand der Ziele
36
36
  log Verlauf der letzten Läufe (Changelog)
37
37
  doctor Fehlende Arbeitsverzeichnisse finden und Vergessen anbieten
@@ -446,6 +446,8 @@ Häufige Fälle:
446
446
 
447
447
  Siehe auch: hmgr help dir, hmgr help link, hmgr help update, hmgr help harness`;
448
448
  const UPDATE = `hmgr update
449
+ hmgr update <skill>
450
+ hmgr update --quelle <teilstring>
449
451
 
450
452
  Holt für jeden Checkout unter ~/.hmgr den neuesten Stand seines Refs und zieht bei Harnesses mit
451
453
  Plugin-Fähigkeit deren Plugins nach (E14). Die Links zeigen weiter auf dieselben Pfade und müssen
@@ -484,10 +486,21 @@ aufgeräumter Checkout, ein Fehler), schreibt update denselben Eintrag auch in ~
484
486
  nicht im Verlauf: „claude plugin marketplace update" liefert keine Auskunft, ob sich etwas geändert
485
487
  hat, eine Zeile dazu wäre geschönt.
486
488
 
487
- Schalter: keine.
489
+ Eingeschränkter Lauf (E89): hmgr update <skill> zieht nur die Quelle nach, in der der Skill liegt
490
+ (liegt der Skill in mehreren Quellen, bricht update ab, dann --quelle nutzen). hmgr update --quelle <teilstring> nimmt ein Stück
491
+ von URL, Pfad oder Anzeigenamen der Quelle, Groß- und Kleinschreibung egal; passt keine oder
492
+ mehr als eine, bricht update mit der Liste ab. Beides zusammen geht nicht. Der Lauf umfasst die
493
+ Checkouts dieser Quelle und die Bundles, deren Definition in ihr liegt. Es entfallen: Plugins,
494
+ Aufräumen verwaister Checkouts, der Check auf fehlende Arbeitsverzeichnisse (doctor) sowie alle
495
+ übrigen Quellen und Bundles. Der Verlaufseintrag entsteht wie im vollen Lauf.
496
+
497
+ Schalter:
498
+ --quelle <teilstring> Nur diese Quelle nachziehen (statt <skill>)
488
499
 
489
500
  Beispiel:
490
501
  hmgr update
502
+ hmgr update mein-skill
503
+ hmgr update --quelle mein-repo
491
504
 
492
505
  Häufige Fälle:
493
506
  „übersprungen … lokale Änderung (E15)": im Checkout liegt eine Änderung, die durch einen Link
package/dist/cli/main.js CHANGED
@@ -93,7 +93,7 @@ const SCHALTER = {
93
93
  bundle: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: [] },
94
94
  mcp: { werte: ['harness', 'ort'], flaggen: [] },
95
95
  sync: { werte: [], flaggen: [] },
96
- update: { werte: [], flaggen: [] },
96
+ update: { werte: ['quelle'], flaggen: [] },
97
97
  status: { werte: [], flaggen: [] },
98
98
  log: { werte: [], flaggen: [] },
99
99
  doctor: { werte: [], flaggen: [] },
@@ -715,8 +715,20 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
715
715
  return 0;
716
716
  }
717
717
  if (kommando === 'update') {
718
- restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
719
- const { checkouts: liste, bundles, aufraeumen, doctor, } = await anwendungsfaelle.aktualisieren({ ausloeser: 'cli' });
718
+ const { rest: updateRest, werte: updateWerte } = schalterLesen(argv.slice(1), 'update');
719
+ restPruefen(updateRest, 1, 'update');
720
+ const updateSkill = updateRest[0];
721
+ const updateHinweis = updateWerte.get('quelle');
722
+ if (updateSkill !== undefined && updateHinweis !== undefined)
723
+ throw new Error('update nimmt entweder einen Skill oder --quelle, nicht beides. hmgr help update zeigt die Verwendung.');
724
+ // Eingeschränkter Lauf (E89): nur die Quelle des Skills bzw. die genannte, ohne Plugins,
725
+ // Aufräumen und doctor.
726
+ const nurQuelle = updateSkill !== undefined
727
+ ? (await anwendungsfaelle.skillFinden(updateSkill)).quelle
728
+ : updateHinweis !== undefined
729
+ ? await anwendungsfaelle.quelleZuHinweis(updateHinweis)
730
+ : undefined;
731
+ const { checkouts: liste, bundles, aufraeumen, doctor, } = await anwendungsfaelle.aktualisieren({ ausloeser: 'cli', ...(nurQuelle === undefined ? {} : { quelle: nurQuelle }) });
720
732
  if (liste.length === 0)
721
733
  console.log('Keine Checkouts.');
722
734
  for (const { checkout, aktualisierung, geaenderteSkills } of liste) {
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.
@@ -676,6 +686,34 @@ function fensterOeffnen() {
676
686
  if (verlaufBloecke > 0) break;
677
687
  }
678
688
  var verlaufText = document.getElementById('verlauf')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
689
+ // Nachziehen je Quelle (E89): das Fixture hat im skills-repo einen neuen Upstream-Commit
690
+ // (fixtureAufbauen), den erst ein Lauf für diese Quelle holt. Erst der Knopf der
691
+ // anderen Quelle (kaputt-repo, ohne Checkout): der Verlauf bleibt, wie er ist, obwohl
692
+ // das skills-repo hinterherhinkt. Dann der Knopf des skills-repo: ein neuer Block, der
693
+ // nur diese Quelle nennt.
694
+ var nachziehenFehler = null;
695
+ var nachziehenKnopfZahl = document.querySelectorAll('#quellenliste .nachziehen').length;
696
+ var nachziehenBloeckeStart = document.querySelectorAll('#verlauf .block').length;
697
+ var nachziehenBloeckeNachAnderer = -1;
698
+ var nachziehenBloeckeNachEigener = -1;
699
+ var nachziehenErsterBlock = null;
700
+ try {
701
+ var nachziehenLauf = async (label) => {
702
+ var knopf = document.querySelector('#quellenliste button[aria-label="' + label + '"]');
703
+ if (!knopf) throw new Error('Knopf fehlt: ' + label);
704
+ knopf.click();
705
+ for (var w = 0; w < 100; w++) {
706
+ await new Promise((r) => setTimeout(r, 100));
707
+ if (!document.getElementById('updatestart').disabled) break;
708
+ }
709
+ return document.querySelectorAll('#verlauf .block').length;
710
+ };
711
+ nachziehenBloeckeNachAnderer = await nachziehenLauf('kaputt-repo nachziehen');
712
+ nachziehenBloeckeNachEigener = await nachziehenLauf('skills-repo nachziehen');
713
+ nachziehenErsterBlock = document.querySelector('#verlauf .block')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
714
+ } catch (fehler) {
715
+ nachziehenFehler = String(fehler);
716
+ }
679
717
  // E60: Beschreibung in der Ziel-Tabelle (nicht nur im Katalog), „manuell" entfällt,
680
718
  // Zahnrad statt Sonne am Einstellungen-Knopf.
681
719
  var zielBeschreibung = document.querySelector('#zielliste .linkliste .was')?.textContent ?? null;
@@ -1149,6 +1187,12 @@ function fensterOeffnen() {
1149
1187
  verlaufVorhanden,
1150
1188
  verlaufText,
1151
1189
  verlaufBloecke,
1190
+ nachziehenFehler,
1191
+ nachziehenKnopfZahl,
1192
+ nachziehenBloeckeStart,
1193
+ nachziehenBloeckeNachAnderer,
1194
+ nachziehenBloeckeNachEigener,
1195
+ nachziehenErsterBlock,
1152
1196
  zielsucheVorhanden,
1153
1197
  zielzahlNachSuche,
1154
1198
  zielKartenNachSucheZahl,
@@ -1168,6 +1212,34 @@ function fensterOeffnen() {
1168
1212
  // E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
1169
1213
  // index.html), er lässt sich nur hier im Main-Prozess ablesen.
1170
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
+ })
1171
1243
  .finally(() => app.quit());
1172
1244
  }, 3000);
1173
1245
  });
@@ -1181,7 +1253,10 @@ function fensterOeffnen() {
1181
1253
  fenster.webContents.once('did-finish-load', () => {
1182
1254
  fenster.webContents.send('hmgr:aktualisierenAnstossen', 'start');
1183
1255
  });
1184
- 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);
1185
1260
  fenster.on('closed', () => clearInterval(intervall));
1186
1261
  }
1187
1262
  // Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
@@ -1190,7 +1265,10 @@ function fensterOeffnen() {
1190
1265
  // Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
1191
1266
  if (!smoketest) {
1192
1267
  const fenster = hauptfenster;
1193
- fenster.on('focus', () => neuLadenAnstossen());
1268
+ fenster.on('focus', () => {
1269
+ neuLadenAnstossen();
1270
+ plattenVersionPruefen();
1271
+ });
1194
1272
  void beobachtetePfade().then((pfade) => {
1195
1273
  if (fenster.isDestroyed())
1196
1274
  return; // Fenster schon zu, während die Pfade ermittelt wurden
@@ -1247,7 +1325,8 @@ function updatePruefen() {
1247
1325
  .catch((fehler) => {
1248
1326
  console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
1249
1327
  });
1250
- selbstUpdateMelden({ art: 'gui', version: guiVersion });
1328
+ geladenesPaketVorhanden = true;
1329
+ selbstUpdateMelden({ art: 'gui', version: guiVersion, neuStartNoetig: 'geladen' });
1251
1330
  });
1252
1331
  }
1253
1332
  void autoUpdater.checkForUpdatesAndNotify().catch((fehler) => {
@@ -1264,6 +1343,44 @@ function selbstUpdateMelden(nachricht) {
1264
1343
  return;
1265
1344
  hauptfenster.webContents.send('hmgr:selbstUpdate', nachricht);
1266
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
+ }
1267
1384
  /**
1268
1385
  * Die daneben installierte hmgr-CLI gegen npm (E77, F83=O1). Läuft auf allen Plattformen — anders
1269
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">
@@ -2629,7 +2629,9 @@
2629
2629
  standText();
2630
2630
  }
2631
2631
 
2632
- async function aktualisierenLauf(ausloeser) {
2632
+ // Mit `quelle` (E89, Knopf „Nachziehen" der Quellenliste) läuft nur diese Quelle, sonst alles.
2633
+ // Bericht, Stand-Zeile und Verlauf sind dieselben wie beim Kringel.
2634
+ async function aktualisierenLauf(ausloeser, quelle) {
2633
2635
  var knopf = $('updatestart');
2634
2636
  if (knopf.disabled) return;
2635
2637
  knopf.disabled = true;
@@ -2637,7 +2639,9 @@
2637
2639
  standLaeuft = true;
2638
2640
  standText();
2639
2641
  var lauf = rufe(function () {
2640
- return window.hmgr.aktualisieren({ rueckfragen: ausloeser !== 'klick', ausloeser: ausloeser });
2642
+ var args = { rueckfragen: ausloeser !== 'klick', ausloeser: ausloeser };
2643
+ if (quelle) args.quelle = quelle;
2644
+ return window.hmgr.aktualisieren(args);
2641
2645
  });
2642
2646
  // Jeder Lauf prüft auch hmgr selbst (E80): angestoßen, nicht abgewartet — ein Fund meldet sich
2643
2647
  // später von sich aus in der Seitenleiste (onSelbstUpdate).
@@ -2664,11 +2668,39 @@
2664
2668
  die CLI installiert die GUI nicht selbst (F82). Die CLI-Zeile ist seit E82 eine Warnung, keine
2665
2669
  bloße Ansage: eine alte CLI (vor 0.0.7, kennt --version nicht) schreibt dieselben Dateien
2666
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
+
2667
2696
  function selbstUpdateZeigen(nachricht) {
2668
2697
  if (nachricht.art === 'gui') {
2669
- $('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';
2670
2701
  $('selbstupdate-gui').hidden = false;
2671
2702
  $('selbstupdate-neustart').hidden = false;
2703
+ neustartAnbieten(nachricht.version);
2672
2704
  } else if (nachricht.art === 'cli') {
2673
2705
  var p = $('selbstupdate-cli');
2674
2706
  p.textContent = '';
@@ -3539,6 +3571,17 @@
3539
3571
  stift.appendChild(stiftSvg);
3540
3572
  stift.addEventListener('click', function () { oeffneQuelleUmbenennen(q); });
3541
3573
  knoepfe.appendChild(stift);
3574
+ // Nur diese Quelle nachziehen (E89): wie der Kringel in der Update-Ansicht, aber ohne
3575
+ // Plugins, Aufräumen und übrige Quellen. Der Renderer kennt nur Text und Art (E54), die
3576
+ // Quelle für den Kern baut er daraus.
3577
+ var nachziehen = e('button', 'knopf leise klein nachziehen', 'Nachziehen');
3578
+ nachziehen.type = 'button';
3579
+ nachziehen.setAttribute('aria-label', q.name + ' nachziehen');
3580
+ nachziehen.addEventListener('click', function () {
3581
+ var quelle = q.art === 'lokal' ? { art: 'lokal', pfad: q.text } : { art: 'git', url: q.text };
3582
+ aktualisierenLauf('klick', quelle);
3583
+ });
3584
+ knoepfe.appendChild(nachziehen);
3542
3585
  var weg = e('button', 'knopf leise klein', 'Entfernen …');
3543
3586
  weg.type = 'button';
3544
3587
  weg.setAttribute('aria-label', q.name + ' entfernen');
@@ -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
+ }
@@ -1109,7 +1109,9 @@ export class Anwendungsfaelle {
1109
1109
  async bundleAbgleich(args) {
1110
1110
  const zustand = await this.p.zustand.lesen();
1111
1111
  const zielSchl = args?.ziel === undefined ? undefined : zielSchluessel(args.ziel);
1112
+ const quelleSchl = args?.quelle === undefined ? undefined : quelleSchluessel(args.quelle);
1112
1113
  const installationen = zustand.bundleInstallationen.filter((i) => (args?.bundle === undefined || i.bundle === args.bundle) &&
1114
+ (quelleSchl === undefined || quelleSchluessel(i.quelle) === quelleSchl) &&
1113
1115
  (zielSchl === undefined || zielSchluessel(Anwendungsfaelle.installationZiel(i)) === zielSchl));
1114
1116
  const ergebnis = [];
1115
1117
  for (const installation of installationen) {
@@ -1274,6 +1276,22 @@ export class Anwendungsfaelle {
1274
1276
  if ((await this.bindungFinden(ziel)) !== undefined)
1275
1277
  throw new ZeigerSchreibenGesperrt(ziel);
1276
1278
  }
1279
+ /**
1280
+ * Die eine geführte Quelle zu einem Teilstring (E89, `hmgr update --quelle`): verglichen wird mit
1281
+ * Schreibweise (URL/Pfad), Schlüssel und Anzeigenamen, Groß- und Kleinschreibung egal. Kein Treffer
1282
+ * und mehrere Treffer sind Fehler, mit der Liste, aus der sich der Nutzer eingrenzen kann.
1283
+ */
1284
+ async quelleZuHinweis(hinweis) {
1285
+ const quellen = (await this.p.zustand.lesen()).quellen;
1286
+ const klein = hinweis.toLowerCase();
1287
+ const treffer = quellen.filter((q) => [quelleAlsText(q), quelleSchluessel(q), q.name ?? ''].some((t) => t.toLowerCase().includes(klein)));
1288
+ const zeilen = (liste) => liste.map((q) => ` ${quelleAlsText(q)}${q.name === undefined ? '' : ` (${q.name})`}`).join('\n');
1289
+ if (treffer.length === 1)
1290
+ return treffer[0];
1291
+ if (treffer.length === 0)
1292
+ throw new Error(`Keine Quelle passt auf „${hinweis}".${quellen.length === 0 ? '' : `\nBekannte Quellen:\n${zeilen(quellen)}`}`);
1293
+ throw new Error(`Mehrere Quellen passen auf „${hinweis}", bitte eingrenzen:\n${zeilen(treffer)}`);
1294
+ }
1277
1295
  /** Skill über alle Quellen suchen. Mehrere Treffer sind nur mit Quellenhinweis erlaubt. */
1278
1296
  async skillFinden(name, quelleHinweis) {
1279
1297
  const zustand = await this.p.zustand.lesen();
@@ -1759,11 +1777,18 @@ export class Anwendungsfaelle {
1759
1777
  * ergeben hat: unveränderte oder übersprungene Checkouts und Bundles ohne Änderung erzeugen
1760
1778
  * nichts, ein leerer Lauf bleibt ganz aus dem Changelog. `ausloeser` steht mit im Eintrag, damit
1761
1779
  * der Verlauf zeigt, ob ein Fund vom automatischen Lauf, einem Klick oder der CLI kam.
1780
+ *
1781
+ * Mit `quelle` (E89) läuft nur diese geführte Quelle: ihre Checkouts, ihr Katalog-Fetch und die
1782
+ * Bundle-Installationen, deren Definition in ihr liegt. Plugins, Aufräumen verwaister Checkouts
1783
+ * und doctor entfallen (leere Ergebnisse), weil sie den Gesamtbestand betreffen.
1762
1784
  */
1763
1785
  async aktualisieren(args) {
1764
1786
  const rueckfragen = args?.rueckfragen ?? true;
1765
1787
  const ausloeser = args?.ausloeser ?? 'cli';
1766
- const bekannt = new Map((await this.p.zustand.lesen()).quellen.map((q) => [quelleSchluessel(q), q]));
1788
+ const nurSchluessel = args?.quelle === undefined ? undefined : quelleSchluessel(args.quelle);
1789
+ const bekannt = new Map((await this.p.zustand.lesen()).quellen
1790
+ .filter((q) => nurSchluessel === undefined || quelleSchluessel(q) === nurSchluessel)
1791
+ .map((q) => [quelleSchluessel(q), q]));
1767
1792
  const checkouts = [];
1768
1793
  for (const checkout of await this.p.quellen.checkouts()) {
1769
1794
  const gefuehrt = bekannt.get(quelleSchluessel(checkout.quelle));
@@ -1805,7 +1830,7 @@ export class Anwendungsfaelle {
1805
1830
  }
1806
1831
  // Nach den Checkouts die Plugins nachziehen (E14).
1807
1832
  const pluginHarnesses = [];
1808
- for (const harness of await this.vorhandeneHarnesses()) {
1833
+ for (const harness of nurSchluessel !== undefined ? [] : await this.vorhandeneHarnesses()) {
1809
1834
  if (!kannPlugins(harness))
1810
1835
  continue;
1811
1836
  await harness.plugins.aktualisieren();
@@ -1814,9 +1839,13 @@ export class Anwendungsfaelle {
1814
1839
  // Bundles vor dem Aufräumen (E69, F64=O1): der Abgleich legt und entfernt Links, und erst
1815
1840
  // danach steht fest, in welchen Checkout noch ein Link zeigt. Ein Fehlschlag je Bundle steckt
1816
1841
  // in seinem Eintrag, er bricht den Lauf nicht ab — wie eine unerreichbare Quelle (E51).
1817
- const bundles = await this.bundleAbgleich();
1818
- const aufraeumen = await this.verwaisteAufraeumen(checkouts, rueckfragen);
1819
- const doctorErgebnis = await this.doctor({ rueckfragen });
1842
+ const bundles = await this.bundleAbgleich(args?.quelle === undefined ? undefined : { quelle: args.quelle });
1843
+ const aufraeumen = nurSchluessel !== undefined
1844
+ ? { verwaiste: [], gefragt: false }
1845
+ : await this.verwaisteAufraeumen(checkouts, rueckfragen);
1846
+ const doctorErgebnis = nurSchluessel !== undefined
1847
+ ? { fehlendeArbeitsverzeichnisse: [], vergessen: [] }
1848
+ : await this.doctor({ rueckfragen });
1820
1849
  await this.verlaufVermerken(ausloeser, this.aktualisierungsEreignisse(checkouts, bundles, aufraeumen, doctorErgebnis));
1821
1850
  return { checkouts, pluginHarnesses, bundles, aufraeumen, doctor: doctorErgebnis };
1822
1851
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.19",
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",