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 +15 -2
- package/dist/cli/main.js +15 -3
- package/dist/gui/main.js +121 -4
- package/dist/gui/renderer/index.html +15 -0
- package/dist/gui/renderer/renderer.js +46 -3
- package/dist/gui/selbst-update.js +74 -0
- package/dist/kern/anwendungsfaelle.js +34 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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', () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
1819
|
-
|
|
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
|
}
|