hmgr 0.0.46 → 0.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/atomar-schreiben.js +33 -0
- package/dist/adapter/git.js +215 -10
- package/dist/adapter/sops.js +107 -0
- package/dist/adapter/verlauf-json.js +3 -2
- package/dist/adapter/zustand-json.js +3 -2
- package/dist/aenderungen.json +6 -0
- package/dist/cli/hilfe.js +34 -7
- package/dist/cli/main.js +38 -6
- package/dist/cli/verlauf-anzeige.js +17 -0
- package/dist/gui/renderer/renderer.js +16 -0
- package/dist/kern/anwendungsfaelle.js +119 -17
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Atomares Schreiben für Zustandsdateien (state.json, verlauf.json): Inhalt in eine Temp-Datei im
|
|
2
|
+
// selben Verzeichnis, dann rename über das Ziel. Ein Leser (GUI-Intervall, CLI in einer anderen
|
|
3
|
+
// Sitzung) sieht so immer den alten oder den neuen vollständigen Inhalt, nie eine leere oder halbe Datei.
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { rename, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
/** Unter Windows scheitert rename über eine Datei, die ein anderer Prozess gerade offen hat, mit diesen Codes. */
|
|
7
|
+
const WIEDERHOLBAR = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
8
|
+
const VERSUCHE = 5;
|
|
9
|
+
async function warten(ms) {
|
|
10
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
}
|
|
12
|
+
export async function atomarSchreiben(ziel, inhalt) {
|
|
13
|
+
const temp = `${ziel}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
14
|
+
try {
|
|
15
|
+
await writeFile(temp, inhalt);
|
|
16
|
+
for (let versuch = 1;; versuch++) {
|
|
17
|
+
try {
|
|
18
|
+
await rename(temp, ziel);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
catch (fehler) {
|
|
22
|
+
const code = fehler.code;
|
|
23
|
+
if (versuch >= VERSUCHE || code === undefined || !WIEDERHOLBAR.has(code))
|
|
24
|
+
throw fehler;
|
|
25
|
+
await warten(20 * versuch);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (fehler) {
|
|
30
|
+
await rm(temp, { force: true });
|
|
31
|
+
throw fehler;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/adapter/git.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Git über die CLI, keine Bibliothek. Checkout-Schema und Worktrees nach E22, Discovery nach E11.
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { readFile, readdir, mkdir, realpath, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { readFile, readdir, mkdir, realpath, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
6
|
+
import { SOPS_FILTER, SOPS_MARKE, checkAttrLesen, filterBefehle, musterAuswahl, probe, sopsFehlertext, standardSops, wrapperFinden, } from "./sops.js";
|
|
5
7
|
import { bundleLesen, istBundleDatei } from "./bundle-yaml.js";
|
|
6
8
|
import { marketplaceLesen } from "./marketplace-json.js";
|
|
7
9
|
import { SKILL_DATEI, skillAusPfad, skillName } from "./skill-md.js";
|
|
@@ -131,9 +133,11 @@ const SPERRE_BELEGT = /Unable to create '[^']+\.lock': File exists/;
|
|
|
131
133
|
export class GitQuellen {
|
|
132
134
|
wurzel;
|
|
133
135
|
sperreWartezeitenMs;
|
|
136
|
+
sops;
|
|
134
137
|
constructor(wurzel, optionen = {}) {
|
|
135
138
|
this.wurzel = wurzel;
|
|
136
139
|
this.sperreWartezeitenMs = optionen.sperreWartezeitenMs ?? SPERRE_WARTEZEITEN_MS;
|
|
140
|
+
this.sops = optionen.sops ?? standardSops(homedir());
|
|
137
141
|
}
|
|
138
142
|
/**
|
|
139
143
|
* Wiederholt einen git-Aufruf, wenn eine fremde Sperre ihn blockiert (E99): GUI-Aktionen und der
|
|
@@ -379,15 +383,10 @@ export class GitQuellen {
|
|
|
379
383
|
async checkout(quelle, ref, skills) {
|
|
380
384
|
// Lokale Quellen werden nicht geklont, der Pfad selbst ist der Checkout (B11).
|
|
381
385
|
if (quelle.art === 'lokal')
|
|
382
|
-
return { quelle, ref, pfad: quelle.pfad };
|
|
386
|
+
return { quelle, ref, pfad: quelle.pfad, sopsVerschluesselt: [] };
|
|
383
387
|
const basis = await this.basis(quelle);
|
|
384
388
|
const pfad = worktreePfad(this.wurzel, quelleZerlegen(quelle), ref);
|
|
385
389
|
const neu = !(await existiert(pfad));
|
|
386
|
-
if (neu) {
|
|
387
|
-
await mkdir(path.dirname(pfad), { recursive: true });
|
|
388
|
-
await this.git(['worktree', 'add', '--detach', '--no-checkout', pfad, ref], basis);
|
|
389
|
-
await this.git(['sparse-checkout', 'init', '--no-cone'], pfad);
|
|
390
|
-
}
|
|
391
390
|
const vorhanden = neu
|
|
392
391
|
? []
|
|
393
392
|
: (await this.git(['sparse-checkout', 'list'], pfad)).split('\n').filter((z) => z.length > 0);
|
|
@@ -404,10 +403,201 @@ export class GitQuellen {
|
|
|
404
403
|
// stört nicht — wohl aber sähe ein Nutzer, der den Link im Finder öffnet, den vollen Worktree.
|
|
405
404
|
for (const skill of skills)
|
|
406
405
|
muster.add(skill.pfad === '' ? '/**' : `/${skill.pfad}/`);
|
|
406
|
+
// SOPS-Filter vor dem ersten Schreiben (E115): `sparse-checkout set` und `checkout` schreiben
|
|
407
|
+
// die Dateien, ein später gesetzter Filter ändert an ihnen nichts mehr. Ein bestehender
|
|
408
|
+
// Worktree steht auf seinem eigenen HEAD, ein neuer kommt auf `ref`.
|
|
409
|
+
const sopsVerschluesselt = await this.sopsVorbereiten(neu ? basis : pfad, neu ? ref : 'HEAD', basis, muster);
|
|
410
|
+
if (neu) {
|
|
411
|
+
await mkdir(path.dirname(pfad), { recursive: true });
|
|
412
|
+
await this.git(['worktree', 'add', '--detach', '--no-checkout', pfad, ref], basis);
|
|
413
|
+
await this.git(['sparse-checkout', 'init', '--no-cone'], pfad);
|
|
414
|
+
}
|
|
407
415
|
await this.git(['sparse-checkout', 'set', '--no-cone', '--', ...muster], pfad);
|
|
408
416
|
if (neu)
|
|
409
417
|
await this.git(['checkout', '--detach', ref], pfad);
|
|
410
|
-
return { quelle, ref, pfad };
|
|
418
|
+
return { quelle, ref, pfad, sopsVerschluesselt };
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Dateien eines Refs unter einem SOPS-Filter, den hmgr kennt (E115), beschränkt auf `auswahl`.
|
|
422
|
+
* Der Normalfall kostet einen `ls-tree`: ohne `.gitattributes`, die für eine gewählte Datei
|
|
423
|
+
* gelten kann, oder ohne „sops_" darin endet die Suche dort. Erst dann `check-attr`, mit
|
|
424
|
+
* `--source` (git ≥ 2.40), weil der Basis-Clone bare ist und ein neuer Worktree noch leer.
|
|
425
|
+
* Scheitert ein Aufruf (etwa ein älteres git), gilt die Liste als leer: dann bleibt es beim
|
|
426
|
+
* Auschecken wie vor E115.
|
|
427
|
+
*/
|
|
428
|
+
async sopsKandidaten(cwd, ref, auswahl) {
|
|
429
|
+
try {
|
|
430
|
+
const alle = (await this.git(['ls-tree', '-r', '-z', '--name-only', ref], cwd)).split('\0').filter((z) => z.length > 0);
|
|
431
|
+
const istAttribute = (d) => d === '.gitattributes' || d.endsWith('/.gitattributes');
|
|
432
|
+
const gewaehlt = alle.filter((d) => auswahl(d) && !istAttribute(d));
|
|
433
|
+
if (gewaehlt.length === 0)
|
|
434
|
+
return [];
|
|
435
|
+
// Eine .gitattributes gilt für ihr Verzeichnis und alles darunter.
|
|
436
|
+
const attribute = alle.filter((d) => {
|
|
437
|
+
if (!istAttribute(d))
|
|
438
|
+
return false;
|
|
439
|
+
const verzeichnis = d.slice(0, -'.gitattributes'.length);
|
|
440
|
+
return gewaehlt.some((g) => g.startsWith(verzeichnis));
|
|
441
|
+
});
|
|
442
|
+
if (attribute.length === 0)
|
|
443
|
+
return [];
|
|
444
|
+
const inhalte = await this.dateiInhalte(cwd, ref, attribute);
|
|
445
|
+
if (![...inhalte.values()].some((text) => text.includes('sops_')))
|
|
446
|
+
return [];
|
|
447
|
+
const ausgabe = await this.gitMitEingabe(['check-attr', `--source=${ref}`, '-z', '--stdin', 'filter'], cwd, `${gewaehlt.join('\0')}\0`);
|
|
448
|
+
const werte = checkAttrLesen(ausgabe.toString('utf8'));
|
|
449
|
+
return gewaehlt
|
|
450
|
+
.map((datei) => ({ datei, filter: werte.get(datei) ?? '' }))
|
|
451
|
+
.filter((k) => SOPS_FILTER.includes(k.filter));
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/** Ob `filter.<name>.smudge` wirksam gesetzt ist, global oder im Basis-Clone (E115). */
|
|
458
|
+
async filterGesetzt(basis, name) {
|
|
459
|
+
const wert = await this.git(['config', '--get', `filter.${name}.smudge`], basis).catch(() => '');
|
|
460
|
+
return wert.trim().length > 0;
|
|
461
|
+
}
|
|
462
|
+
/** Filter wie sops-setup.sh im Basis-Clone setzen; er gilt für alle Worktrees der Quelle (E115). */
|
|
463
|
+
async filterSetzen(basis, namen, wrapper) {
|
|
464
|
+
const { clean, smudge } = filterBefehle(this.sops.programm, wrapper);
|
|
465
|
+
for (const name of namen) {
|
|
466
|
+
await this.git(['config', `filter.${name}.clean`, clean], basis);
|
|
467
|
+
await this.git(['config', `filter.${name}.smudge`, smudge], basis);
|
|
468
|
+
await this.git(['config', `filter.${name}.required`, 'true'], basis);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Probe über alle Dateien, Abbruch beim ersten Fehlschlag (E115): nur wenn alle klappen, kommen
|
|
473
|
+
* Filter in Frage. Ein Fehlschlag gilt für alle, denn ohne Filter bleiben alle verschlüsselt.
|
|
474
|
+
*/
|
|
475
|
+
async sopsProbe(cwd, ref, dateien) {
|
|
476
|
+
const inhalte = await this.dateiInhalte(cwd, ref, dateien);
|
|
477
|
+
for (const datei of dateien) {
|
|
478
|
+
const inhalt = inhalte.get(datei);
|
|
479
|
+
if (inhalt === undefined)
|
|
480
|
+
return 'entschluesselung-scheitert';
|
|
481
|
+
const ergebnis = await probe(this.sops.programm, datei, inhalt);
|
|
482
|
+
if (!ergebnis.ok)
|
|
483
|
+
return ergebnis.grund;
|
|
484
|
+
}
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Vor dem Schreiben eines Checkouts (E115): SOPS-Dateien unter den Mustern finden, für Filter,
|
|
489
|
+
* die noch nicht wirksam sind, die Probe machen und bei Erfolg die Filter setzen. Ein schon
|
|
490
|
+
* gesetzter Filter (global oder lokal) bleibt unangetastet und braucht keine Probe. Ergebnis:
|
|
491
|
+
* die Dateien, die verschlüsselt ankommen werden.
|
|
492
|
+
*/
|
|
493
|
+
async sopsVorbereiten(cwd, ref, basis, muster) {
|
|
494
|
+
const kandidaten = await this.sopsKandidaten(cwd, ref, musterAuswahl(muster));
|
|
495
|
+
if (kandidaten.length === 0)
|
|
496
|
+
return [];
|
|
497
|
+
const offen = [];
|
|
498
|
+
for (const name of new Set(kandidaten.map((k) => k.filter)))
|
|
499
|
+
if (!(await this.filterGesetzt(basis, name)))
|
|
500
|
+
offen.push(name);
|
|
501
|
+
if (offen.length === 0)
|
|
502
|
+
return [];
|
|
503
|
+
const betroffen = kandidaten.filter((k) => offen.includes(k.filter)).map((k) => k.datei);
|
|
504
|
+
// Ohne sops-clean.sh keine Filter (F140): git hielte die entschlüsselte Datei sonst sofort für
|
|
505
|
+
// geändert (echtes `sops --encrypt` verschlüsselt jedes Mal anders). Die Probe entfällt dann.
|
|
506
|
+
const wrapper = await wrapperFinden(this.sops.wrapperKandidaten);
|
|
507
|
+
if (wrapper === undefined)
|
|
508
|
+
return betroffen.map((datei) => ({ datei, grund: 'wrapper-fehlt' }));
|
|
509
|
+
const grund = await this.sopsProbe(cwd, ref, betroffen);
|
|
510
|
+
if (grund !== undefined)
|
|
511
|
+
return betroffen.map((datei) => ({ datei, grund }));
|
|
512
|
+
await this.filterSetzen(basis, offen, wrapper);
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
/** SOPS-Dateien eines bestehenden Worktrees samt Inhalt auf der Platte (E115). */
|
|
516
|
+
async sopsAufPlatte(checkout) {
|
|
517
|
+
if (checkout.quelle.art === 'lokal' || !(await existiert(checkout.pfad)))
|
|
518
|
+
return [];
|
|
519
|
+
const kandidaten = await this.sopsKandidaten(checkout.pfad, 'HEAD', () => true);
|
|
520
|
+
const ergebnis = [];
|
|
521
|
+
for (const kandidat of kandidaten) {
|
|
522
|
+
// Was der Sparse-Checkout nicht geschrieben hat, liegt nicht auf der Platte und zählt nicht.
|
|
523
|
+
const inhalt = await readFile(path.join(checkout.pfad, kandidat.datei), 'utf8').catch(() => undefined);
|
|
524
|
+
if (inhalt !== undefined)
|
|
525
|
+
ergebnis.push({ ...kandidat, inhalt });
|
|
526
|
+
}
|
|
527
|
+
return ergebnis;
|
|
528
|
+
}
|
|
529
|
+
async sopsDateien(checkout) {
|
|
530
|
+
return (await this.sopsAufPlatte(checkout)).map((k) => ({
|
|
531
|
+
datei: k.datei,
|
|
532
|
+
verschluesselt: k.inhalt.includes(SOPS_MARKE),
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Bestand (E115): eine SOPS-Datei, die vor E115 verschlüsselt ankam, bleibt so, denn git hält
|
|
537
|
+
* sie für unverändert. Wo die Probe klappt: Filter setzen (falls noch nicht wirksam), Datei
|
|
538
|
+
* löschen, `git checkout -- <datei>` schreibt sie durch den smudge-Filter neu. Danach muss sie
|
|
539
|
+
* Klartext sein und `git status` für sie leer, sonst gilt das Neuschreiben als gescheitert.
|
|
540
|
+
* Eine Datei, die nicht mehr dem Commit entspricht (von Hand geändert), fasst hmgr nicht an —
|
|
541
|
+
* verglichen werden die Bytes mit dem Blob, unabhängig von Filtern. Scheitert `checkout`, kommt
|
|
542
|
+
* der alte Inhalt zurück, damit nie eine Datei fehlt.
|
|
543
|
+
*/
|
|
544
|
+
async sopsReparieren(checkout) {
|
|
545
|
+
if (checkout.quelle.art === 'lokal')
|
|
546
|
+
return [];
|
|
547
|
+
const verschluesselt = (await this.sopsAufPlatte(checkout)).filter((k) => k.inhalt.includes(SOPS_MARKE));
|
|
548
|
+
if (verschluesselt.length === 0)
|
|
549
|
+
return [];
|
|
550
|
+
const ergebnis = [];
|
|
551
|
+
const blobs = await this.dateiInhalte(checkout.pfad, 'HEAD', verschluesselt.map((k) => k.datei));
|
|
552
|
+
const unveraendert = verschluesselt.filter((k) => {
|
|
553
|
+
if (blobs.get(k.datei) === k.inhalt)
|
|
554
|
+
return true;
|
|
555
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'verschluesselt', grund: 'lokale-aenderung' });
|
|
556
|
+
return false;
|
|
557
|
+
});
|
|
558
|
+
if (unveraendert.length === 0)
|
|
559
|
+
return ergebnis;
|
|
560
|
+
const basis = await this.basis(checkout.quelle);
|
|
561
|
+
const offen = [];
|
|
562
|
+
for (const name of new Set(unveraendert.map((k) => k.filter)))
|
|
563
|
+
if (!(await this.filterGesetzt(basis, name)))
|
|
564
|
+
offen.push(name);
|
|
565
|
+
// Müssen Filter erst gesetzt werden, braucht es sops-clean.sh (F140), sonst bleibt alles, wie es ist.
|
|
566
|
+
const wrapper = offen.length === 0 ? undefined : await wrapperFinden(this.sops.wrapperKandidaten);
|
|
567
|
+
if (offen.length > 0 && wrapper === undefined) {
|
|
568
|
+
for (const k of unveraendert)
|
|
569
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'verschluesselt', grund: 'wrapper-fehlt' });
|
|
570
|
+
return ergebnis;
|
|
571
|
+
}
|
|
572
|
+
const grund = await this.sopsProbe(checkout.pfad, 'HEAD', unveraendert.map((k) => k.datei));
|
|
573
|
+
if (grund !== undefined) {
|
|
574
|
+
for (const k of unveraendert)
|
|
575
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'verschluesselt', grund });
|
|
576
|
+
return ergebnis;
|
|
577
|
+
}
|
|
578
|
+
if (wrapper !== undefined)
|
|
579
|
+
await this.filterSetzen(basis, offen, wrapper);
|
|
580
|
+
for (const k of unveraendert) {
|
|
581
|
+
const datei = path.join(checkout.pfad, k.datei);
|
|
582
|
+
try {
|
|
583
|
+
await rm(datei, { force: true });
|
|
584
|
+
await this.git(['checkout', '--', k.datei], checkout.pfad);
|
|
585
|
+
}
|
|
586
|
+
catch (fehler) {
|
|
587
|
+
await writeFile(datei, k.inhalt);
|
|
588
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'fehlgeschlagen', fehler: fehler instanceof Error ? fehler.message : String(fehler) });
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const neu = await readFile(datei, 'utf8').catch(() => undefined);
|
|
592
|
+
const status = await this.git(['--no-optional-locks', 'status', '--porcelain', '--', k.datei], checkout.pfad).catch(() => 'unlesbar');
|
|
593
|
+
if (neu === undefined || neu.includes(SOPS_MARKE))
|
|
594
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'fehlgeschlagen', fehler: 'nach dem Neuschreiben noch verschlüsselt' });
|
|
595
|
+
else if (status.trim().length > 0)
|
|
596
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'fehlgeschlagen', fehler: 'git meldet die Datei danach als geändert' });
|
|
597
|
+
else
|
|
598
|
+
ergebnis.push({ datei: k.datei, ergebnis: 'repariert' });
|
|
599
|
+
}
|
|
600
|
+
return ergebnis;
|
|
411
601
|
}
|
|
412
602
|
/**
|
|
413
603
|
* Alle Checkouts unter der Wurzel, aus dem Dateisystem gelesen (E19). Die Quelle kommt aus der
|
|
@@ -446,7 +636,15 @@ export class GitQuellen {
|
|
|
446
636
|
async aktualisieren(checkout) {
|
|
447
637
|
if (checkout.quelle.art === 'lokal')
|
|
448
638
|
return { ergebnis: 'unveraendert' };
|
|
449
|
-
|
|
639
|
+
// Auch `status` kann an einem SOPS-Filter scheitern (E115): nur dieser Checkout schlägt fehl.
|
|
640
|
+
let lokalGeaendert;
|
|
641
|
+
try {
|
|
642
|
+
lokalGeaendert = await this.lokaleAenderungen(checkout);
|
|
643
|
+
}
|
|
644
|
+
catch (fehler) {
|
|
645
|
+
return { ergebnis: 'fehlgeschlagen', fehler: sopsFehlertext(fehler) };
|
|
646
|
+
}
|
|
647
|
+
if (lokalGeaendert)
|
|
450
648
|
return { ergebnis: 'uebersprungen', grund: 'lokale-aenderung' }; // E15
|
|
451
649
|
const basis = await this.basis(checkout.quelle);
|
|
452
650
|
const von = (await this.git(['rev-parse', 'HEAD'], checkout.pfad)).trim();
|
|
@@ -460,7 +658,14 @@ export class GitQuellen {
|
|
|
460
658
|
const nach = (await this.git(['rev-parse', checkout.ref], basis)).trim();
|
|
461
659
|
if (von === nach)
|
|
462
660
|
return { ergebnis: 'unveraendert' };
|
|
463
|
-
|
|
661
|
+
try {
|
|
662
|
+
await this.git(['checkout', '--detach', checkout.ref], checkout.pfad);
|
|
663
|
+
}
|
|
664
|
+
catch (fehler) {
|
|
665
|
+
// Ein required-Filter, der nicht entschlüsseln kann, bricht das Nachziehen ab (E115). Das
|
|
666
|
+
// trifft nur diesen Checkout, die übrigen laufen weiter.
|
|
667
|
+
return { ergebnis: 'fehlgeschlagen', fehler: sopsFehlertext(fehler) };
|
|
668
|
+
}
|
|
464
669
|
return { ergebnis: 'aktualisiert', von, nach };
|
|
465
670
|
}
|
|
466
671
|
/**
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// SOPS-Filter für Checkouts (E115): Probe-Entschlüsselung, Filterbefehle und das Lesen von
|
|
2
|
+
// `git check-attr`. Die Filter selbst setzt GitQuellen (git.ts) im Basis-Clone; hier liegt nur, was
|
|
3
|
+
// ohne git auskommt. Vorlage der Filterbefehle: sops-setup.sh im Skill sops-secrets-manager.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { access, constants } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
/** Die Filternamen, die hmgr kennt und selbst einrichtet (E115). */
|
|
8
|
+
export const SOPS_FILTER = ['sops_dotenv', 'sops_yaml', 'sops_generic'];
|
|
9
|
+
/** Woran hmgr eine noch verschlüsselte SOPS-Datei im Arbeitsbaum erkennt. */
|
|
10
|
+
export const SOPS_MARKE = 'ENC[';
|
|
11
|
+
/** Die Einstellung im Betrieb: sops vom PATH, sops-clean.sh unter /usr/local/bin oder ~/bin. */
|
|
12
|
+
export function standardSops(home) {
|
|
13
|
+
return {
|
|
14
|
+
programm: 'sops',
|
|
15
|
+
wrapperKandidaten: ['/usr/local/bin/sops-clean.sh', path.join(home, 'bin', 'sops-clean.sh')],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Ein Wort für die Shell, die git für Filterbefehle startet: unverändert, wenn nichts darin die
|
|
20
|
+
* Shell stört, sonst in einfachen Anführungszeichen. So bricht ein Home-Pfad mit Leerzeichen nicht.
|
|
21
|
+
*/
|
|
22
|
+
export function shellWort(wert) {
|
|
23
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(wert))
|
|
24
|
+
return wert;
|
|
25
|
+
return `'${wert.replace(/'/g, `'\\''`)}'`;
|
|
26
|
+
}
|
|
27
|
+
/** clean- und smudge-Befehl wie sops-setup.sh; `%f` setzt git selbst (schon für die Shell gequotet). */
|
|
28
|
+
export function filterBefehle(programm, wrapper) {
|
|
29
|
+
const sops = shellWort(programm);
|
|
30
|
+
const verschluesseln = `${sops} --encrypt --filename-override %f /dev/stdin`;
|
|
31
|
+
return {
|
|
32
|
+
clean: wrapper === undefined ? verschluesseln : `${shellWort(wrapper)} %f ${verschluesseln}`,
|
|
33
|
+
smudge: `${sops} --decrypt --filename-override %f /dev/stdin`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Der erste ausführbare Kandidat für sops-clean.sh, sonst undefined. */
|
|
37
|
+
export async function wrapperFinden(kandidaten) {
|
|
38
|
+
for (const kandidat of kandidaten) {
|
|
39
|
+
const ausfuehrbar = await access(kandidat, constants.X_OK).then(() => true, () => false);
|
|
40
|
+
if (ausfuehrbar)
|
|
41
|
+
return kandidat;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Entschlüsselt `inhalt` probeweise so, wie es der smudge-Filter täte. Der Klartext wird verworfen,
|
|
47
|
+
* es zählt nur der Exit-Code. Fehlt das Programm, heißt das „sops fehlt", kein Absturz.
|
|
48
|
+
*/
|
|
49
|
+
export async function probe(programm, datei, inhalt) {
|
|
50
|
+
return await new Promise((erfuellen) => {
|
|
51
|
+
let fertig = false;
|
|
52
|
+
const ende = (ergebnis) => {
|
|
53
|
+
if (fertig)
|
|
54
|
+
return;
|
|
55
|
+
fertig = true;
|
|
56
|
+
erfuellen(ergebnis);
|
|
57
|
+
};
|
|
58
|
+
const kind = spawn(programm, ['--decrypt', '--filename-override', datei, '/dev/stdin'], {
|
|
59
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
60
|
+
timeout: 60_000,
|
|
61
|
+
killSignal: 'SIGKILL',
|
|
62
|
+
});
|
|
63
|
+
kind.on('error', (fehler) => ende({ ok: false, grund: fehler.code === 'ENOENT' ? 'sops-fehlt' : 'entschluesselung-scheitert' }));
|
|
64
|
+
kind.on('close', (code) => ende(code === 0 ? { ok: true } : { ok: false, grund: 'entschluesselung-scheitert' }));
|
|
65
|
+
// Endet sops vor dem Lesen, schreibt Node in eine geschlossene Pipe: das ist kein eigener Fehler.
|
|
66
|
+
kind.stdin.on('error', () => { });
|
|
67
|
+
kind.stdin.end(inhalt);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fehlertext für einen gescheiterten git-Aufruf beim Nachziehen (E115): Scheitert er an einem
|
|
72
|
+
* SOPS-Filter, steht vorn ein verständlicher Satz, die Meldung von git folgt dahinter.
|
|
73
|
+
*/
|
|
74
|
+
export function sopsFehlertext(fehler) {
|
|
75
|
+
const meldung = fehler instanceof Error ? fehler.message : String(fehler);
|
|
76
|
+
// git meldet je nach Sprache anders; sicher drin stehen der Filtername oder der sops-Befehl.
|
|
77
|
+
if (!/sops_(?:dotenv|yaml|generic)|--decrypt --filename-override/.test(meldung))
|
|
78
|
+
return meldung;
|
|
79
|
+
return `Eine mit SOPS verschlüsselte Datei ließ sich nicht entschlüsseln (sops fehlt oder der Schlüssel passt nicht), dieser Checkout wurde nicht nachgezogen. git meldet: ${meldung}`;
|
|
80
|
+
}
|
|
81
|
+
/** Ausgabe von `git check-attr -z … filter`: je Datei drei NUL-getrennte Felder Pfad, Attribut, Wert. */
|
|
82
|
+
export function checkAttrLesen(ausgabe) {
|
|
83
|
+
const teile = ausgabe.split('\0');
|
|
84
|
+
const ergebnis = new Map();
|
|
85
|
+
for (let i = 0; i + 2 < teile.length; i += 3)
|
|
86
|
+
ergebnis.set(teile[i], teile[i + 2]);
|
|
87
|
+
return ergebnis;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Welche Dateien ein Sparse-Checkout mit diesen Mustern schreibt (E11): `/*` die Dateien an der
|
|
91
|
+
* Wurzel, `/**` alles, `/<pfad>/` alles darunter. Ausschlüsse (`!…`) nehmen nur Verzeichnisse an
|
|
92
|
+
* der Wurzel heraus, die über `/*` ohnehin nicht als Datei zählen.
|
|
93
|
+
*/
|
|
94
|
+
export function musterAuswahl(muster) {
|
|
95
|
+
let wurzelDateien = false;
|
|
96
|
+
let alles = false;
|
|
97
|
+
const praefixe = [];
|
|
98
|
+
for (const m of muster) {
|
|
99
|
+
if (m === '/*')
|
|
100
|
+
wurzelDateien = true;
|
|
101
|
+
else if (m === '/**')
|
|
102
|
+
alles = true;
|
|
103
|
+
else if (!m.startsWith('!') && m.startsWith('/') && m.endsWith('/') && m.length > 2)
|
|
104
|
+
praefixe.push(m.slice(1));
|
|
105
|
+
}
|
|
106
|
+
return (datei) => alles || (wurzelDateien && !datei.includes('/')) || praefixe.some((praefix) => datei.startsWith(praefix));
|
|
107
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Der Verlauf als eine JSON-Datei, Standard ~/.hmgr/verlauf.json (E80). Analog ZustandJson
|
|
2
2
|
// (zustand-json.ts), aber ohne Fassungs-Migration: das Format ist neu, `fassung: 1` reicht.
|
|
3
|
-
import { mkdir, readFile
|
|
3
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { leererVerlauf } from "../ports/verlauf.js";
|
|
6
|
+
import { atomarSchreiben } from "./atomar-schreiben.js";
|
|
6
7
|
import { ohneBom } from "./datei-text.js";
|
|
7
8
|
/** Höchstzahl der Einträge in der Datei (E80): ein Changelog, keine Historie ohne Ende. */
|
|
8
9
|
const MAX_EINTRAEGE = 20;
|
|
@@ -34,6 +35,6 @@ export class VerlaufJson {
|
|
|
34
35
|
await mkdir(path.dirname(this.pfad), { recursive: true });
|
|
35
36
|
// Neueste zuerst kappen, nicht sortieren: die Reihenfolge liegt beim Aufrufer.
|
|
36
37
|
const gekappt = { gemerkt: daten.gemerkt, eintraege: daten.eintraege.slice(0, MAX_EINTRAEGE) };
|
|
37
|
-
await
|
|
38
|
+
await atomarSchreiben(this.pfad, `${JSON.stringify({ fassung: 1, ...gekappt }, null, 2)}\n`);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Lokaler Zustand als eine JSON-Datei, Standard ~/.hmgr/state.json (E29).
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { mkdir, readFile
|
|
3
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { ZUSTAND_FASSUNG, leererZustand, } from "../ports/zustand.js";
|
|
6
|
+
import { atomarSchreiben } from "./atomar-schreiben.js";
|
|
6
7
|
import { ohneBom } from "./datei-text.js";
|
|
7
8
|
/** Der gelesene Text als Zustand. Fehlende Felder bekommen den Wert, der dem Bestand entspricht. */
|
|
8
9
|
function deuten(roh) {
|
|
@@ -54,6 +55,6 @@ export class ZustandJson {
|
|
|
54
55
|
await mkdir(path.dirname(this.pfad), { recursive: true });
|
|
55
56
|
// Immer in der aktuellen Fassung, egal welche gelesen wurde: das Lesen hat den Bestand schon
|
|
56
57
|
// auf die aktuelle Form gebracht, ein alter Vermerk in der Datei wäre danach eine Lüge.
|
|
57
|
-
await
|
|
58
|
+
await atomarSchreiben(this.pfad, `${JSON.stringify({ ...zustand, fassung: ZUSTAND_FASSUNG }, null, 2)}\n`);
|
|
58
59
|
}
|
|
59
60
|
}
|
package/dist/aenderungen.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"fassung": 1,
|
|
3
3
|
"versionen": {
|
|
4
|
+
"0.0.48": [
|
|
5
|
+
"Die Meldung „Unexpected end of JSON input“ taucht nicht mehr auf, wenn hmgr an mehreren Stellen gleichzeitig läuft, etwa die Oberfläche und die Kommandozeile: hmgr schreibt seinen Zustand und den Verlauf jetzt so, dass nie eine halb geschriebene Datei gelesen wird."
|
|
6
|
+
],
|
|
7
|
+
"0.0.47": [
|
|
8
|
+
"Skills mit SOPS-verschlüsselten Dateien kommen jetzt entschlüsselt an, wenn sops, der passende Schlüssel und sops-clean.sh vorhanden sind; bereits verschlüsselt ausgecheckte Dateien repariert hmgr beim nächsten Aktualisieren. Klappt das nicht, bleibt die Datei verschlüsselt und hmgr sagt im Verlauf und in hmgr status, warum."
|
|
9
|
+
],
|
|
4
10
|
"0.0.46": [
|
|
5
11
|
"Bundles lassen sich aufklappen und zeigen ihre Skills mit Beschreibung, auch schon im Dialog „Skills hinzufügen“. Entfernst du einen einzelnen Skill, bleibt das Bundle bestehen und zeigt, was fehlt; mit „Vervollständigen“ oder „Wieder verlinken“ holst du Skills zurück, und „Entfernen“ am Bundle räumt den Rest weg."
|
|
6
12
|
],
|
package/dist/cli/hilfe.js
CHANGED
|
@@ -531,7 +531,9 @@ ohne den übrigen Lauf aufzuhalten. hmgr bundle sync macht dasselbe für ein ein
|
|
|
531
531
|
Danach räumt update verwaiste Ref-Verzeichnisse auf: Checkouts, in die kein Link mehr zeigt, weil
|
|
532
532
|
ein Link inzwischen auf einen anderen Ref umgelegt wurde. Vor dem Löschen fragt hmgr einmal für
|
|
533
533
|
alle Kandidaten. Ohne Bestätigung, also auch ohne Terminal, bleibt alles liegen und wird nur
|
|
534
|
-
gemeldet. Ein verwaister Checkout mit lokaler Änderung wird nie gelöscht (E15).
|
|
534
|
+
gemeldet. Ein verwaister Checkout mit lokaler Änderung wird nie gelöscht (E15). Danach folgen die
|
|
535
|
+
Prüfungen von hmgr doctor: fehlende Arbeitsverzeichnisse und mit SOPS verschlüsselte Dateien, die
|
|
536
|
+
noch verschlüsselt im Checkout liegen (siehe hmgr help doctor).
|
|
535
537
|
|
|
536
538
|
So wird eine Änderung an einem Skill überall wirksam: in der Quelle committen und pushen, dann
|
|
537
539
|
hmgr update.
|
|
@@ -551,7 +553,8 @@ Eingeschränkter Lauf (E89): hmgr update <skill> zieht nur die Quelle nach, in d
|
|
|
551
553
|
von URL, Pfad oder Anzeigenamen der Quelle, Groß- und Kleinschreibung egal; passt keine oder
|
|
552
554
|
mehr als eine, bricht update mit der Liste ab. Beides zusammen geht nicht. Der Lauf umfasst die
|
|
553
555
|
Checkouts dieser Quelle und die Bundles, deren Definition in ihr liegt. Es entfallen: Plugins,
|
|
554
|
-
Aufräumen verwaister Checkouts,
|
|
556
|
+
Aufräumen verwaister Checkouts, die doctor-Prüfungen (fehlende Arbeitsverzeichnisse,
|
|
557
|
+
verschlüsselte SOPS-Dateien) sowie alle
|
|
555
558
|
übrigen Quellen und Bundles. Der Verlaufseintrag entsteht wie im vollen Lauf.
|
|
556
559
|
|
|
557
560
|
Schalter:
|
|
@@ -587,7 +590,10 @@ ausgecheckte Skills (E70). Quellen ohne Skill-Änderung sammelt eine Zeile am En
|
|
|
587
590
|
Ereignisse, je eine Zeile: ein nachgezogenes Bundle mit gelegten und entfernten Links, ein
|
|
588
591
|
aufgeräumter (gelöschter) Checkout, ein Fehler zu einer Quelle oder einem Bundle, dazu
|
|
589
592
|
hmgr-Ereignisse: ein Versionssprung von hmgr selbst (GUI oder CLI, erkannt beim Start bzw. bei
|
|
590
|
-
jedem CLI-Aufruf) oder ein gefundenes Update, das noch nicht installiert ist.
|
|
593
|
+
jedem CLI-Aufruf) oder ein gefundenes Update, das noch nicht installiert ist. Dazu Dateien, die
|
|
594
|
+
eine Quelle mit SOPS verschlüsselt: eine, die verschlüsselt im Checkout blieb, mit Grund (einmal,
|
|
595
|
+
auch direkt nach einem hmgr link), und eine, die hmgr nachträglich entschlüsselt hat (siehe
|
|
596
|
+
hmgr help doctor). Einträge aus der
|
|
591
597
|
Zeit vor E109 zeigen nur „Geändert". Plugins stehen nicht im Verlauf (siehe hmgr help update).
|
|
592
598
|
|
|
593
599
|
Unter einem „installiert"-Eintrag von hmgr stehen die Änderungen: je Version seit der zuvor
|
|
@@ -611,8 +617,21 @@ Prüft, ob jedes gemerkte Arbeitsverzeichnis (hmgr dir add) noch auf der Platte
|
|
|
611
617
|
umbenanntes oder woanders neu geklontes Projekt hinterlässt sonst für immer einen toten Pfad im
|
|
612
618
|
Zustand, den nur hmgr dir forget von Hand entfernt. Bei einem Fund fragt doctor wie hmgr update
|
|
613
619
|
einmal für alle Funde, ob sie vergessen werden sollen; ohne Bestätigung bleiben sie gemeldet, aber
|
|
614
|
-
gemerkt. Anders als update holt doctor nichts aus dem Netz
|
|
615
|
-
|
|
620
|
+
gemerkt. Anders als update holt doctor nichts aus dem Netz.
|
|
621
|
+
|
|
622
|
+
Dazu prüft doctor Dateien, die eine Quelle mit SOPS verschlüsselt (.gitattributes mit
|
|
623
|
+
filter=sops_dotenv, sops_yaml oder sops_generic) und die noch verschlüsselt im Checkout liegen,
|
|
624
|
+
etwa weil sie vor dieser Prüfung ausgecheckt wurden oder sops damals fehlte. Liegt
|
|
625
|
+
sops-clean.sh unter /usr/local/bin oder ~/bin und klappt eine probeweise Entschlüsselung mit sops,
|
|
626
|
+
richtet doctor die git-Filter für die Quelle ein (wie sops-setup.sh, mit sops-clean.sh) und
|
|
627
|
+
schreibt die Datei entschlüsselt neu. Sonst bleibt die Datei, wie sie ist, und doctor nennt den
|
|
628
|
+
Grund: sops-clean.sh fehlt, sops fehlt, der Schlüssel passt nicht, oder die Datei ist im Checkout
|
|
629
|
+
von Hand geändert. Ohne sops-clean.sh richtet hmgr nie Filter ein, weil git die entschlüsselte
|
|
630
|
+
Datei sonst sofort für geändert hielte und hmgr update den Checkout danach immer überspränge.
|
|
631
|
+
Dieselbe Regel gilt schon beim ersten Auschecken (hmgr link).
|
|
632
|
+
Diese Befunde stehen auch im Verlauf (hmgr log), eine verschlüsselt gebliebene Datei nur einmal.
|
|
633
|
+
Fehlende Arbeitsverzeichnisse schreibt doctor nicht in den Verlauf. hmgr update macht dieselbe
|
|
634
|
+
Prüfung im selben Lauf.
|
|
616
635
|
|
|
617
636
|
Schalter: keine.
|
|
618
637
|
|
|
@@ -622,6 +641,11 @@ Beispiel:
|
|
|
622
641
|
Häufige Fälle:
|
|
623
642
|
„liegen lassen … ohne Bestätigung nicht vergessen": die Frage kam ohne Ja zurück. In einem
|
|
624
643
|
Terminal erneut hmgr doctor aufrufen, oder hmgr dir forget <pfad> gezielt.
|
|
644
|
+
„verschlüsselt … sops-clean.sh fehlt": sops-clean.sh ausführbar nach ~/bin oder
|
|
645
|
+
/usr/local/bin legen, dann erneut hmgr doctor.
|
|
646
|
+
„verschlüsselt … sops ist auf diesem Rechner nicht installiert": sops installieren, dann erneut
|
|
647
|
+
hmgr doctor. „… vermutlich fehlt der passende Schlüssel": der Schlüssel dieses Rechners steht
|
|
648
|
+
nicht unter den Empfängern der Datei, oder sops findet ihn nicht.
|
|
625
649
|
|
|
626
650
|
Siehe auch: hmgr help update, hmgr help dir`;
|
|
627
651
|
const STATUS = `hmgr status
|
|
@@ -631,7 +655,8 @@ und Ref, davor die Versionseinschränkung, falls der Zeiger eine nennt. Dahinter
|
|
|
631
655
|
abgeleitete Grund des Links: das Plugin, zu dem er gehört, oder „über Zeiger", von Hand gelegte
|
|
632
656
|
Links bleiben ohne Zusatz. Fremde Einträge und Links ohne bekannte Quelle sind eigens
|
|
633
657
|
gekennzeichnet, dazu kommen die Plugins des Ziels. Am Ende stehen die Harnesses, die auf diesem
|
|
634
|
-
Rechner nicht liegen
|
|
658
|
+
Rechner nicht liegen, und, falls es welche gibt, mit SOPS verschlüsselte Dateien, die noch
|
|
659
|
+
verschlüsselt im Checkout liegen. status repariert nichts, das macht hmgr doctor.
|
|
635
660
|
|
|
636
661
|
Schalter: keine.
|
|
637
662
|
|
|
@@ -644,10 +669,12 @@ Häufige Fälle:
|
|
|
644
669
|
Zustand steht. Mit hmgr source add <url> die Quelle wieder eintragen.
|
|
645
670
|
„Nicht vorhanden": der Harness hat sein Verzeichnis auf diesem Rechner nicht angelegt, hmgr
|
|
646
671
|
überspringt seine Ziele.
|
|
672
|
+
„Noch verschlüsselt im Checkout": hmgr doctor versucht, die Datei zu entschlüsseln, und nennt
|
|
673
|
+
den Grund, wenn es nicht klappt.
|
|
647
674
|
Ein Ziel fehlt: Ziele inaktiver Harness-Typen zeigt status nicht, auch wenn dort noch Links
|
|
648
675
|
liegen. hmgr harness list zeigt den Stand, hmgr harness enable <name> holt die Ziele zurück.
|
|
649
676
|
|
|
650
|
-
Siehe auch: hmgr help adopt, hmgr help sync`;
|
|
677
|
+
Siehe auch: hmgr help adopt, hmgr help sync, hmgr help doctor`;
|
|
651
678
|
const HARNESS = `hmgr harness list
|
|
652
679
|
hmgr harness enable <name>
|
|
653
680
|
hmgr harness disable <name>
|
package/dist/cli/main.js
CHANGED
|
@@ -4,7 +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
|
+
import { checkoutGruppen, quellenAnzeigeName, sammelZeile, sopsGrundText, sopsZeile, verlaufZeitpunkt, } from "./verlauf-anzeige.js";
|
|
8
8
|
import { Anwendungsfaelle, pluginWeltText, } from "../kern/anwendungsfaelle.js";
|
|
9
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";
|
|
10
10
|
import { Agents } from "../adapter/agents.js";
|
|
@@ -19,6 +19,7 @@ import { DateiMcpKatalog } from "../adapter/mcp-katalog.js";
|
|
|
19
19
|
import { cliUpdateHinweis } from "../adapter/npm-registry.js";
|
|
20
20
|
import { paketVersion } from "../adapter/paket-version.js";
|
|
21
21
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
22
|
+
import { standardSops } from "../adapter/sops.js";
|
|
22
23
|
import { TerminalRueckfrage } from "../adapter/terminal.js";
|
|
23
24
|
import { ProzessUmgebung } from "../adapter/umgebung.js";
|
|
24
25
|
import { signalSchreiben } from "../adapter/gui-signal.js";
|
|
@@ -92,7 +93,7 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
92
93
|
});
|
|
93
94
|
const ports = {
|
|
94
95
|
harnesses: [claude, new Hermes(heime, zugriffFuerOrt), new Agents(heime, zugriffFuerOrt)],
|
|
95
|
-
quellen: new GitQuellen(wurzel),
|
|
96
|
+
quellen: new GitQuellen(wurzel, { sops: optionen.sops ?? standardSops(umgebung.home) }),
|
|
96
97
|
ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome, (ziel) => zugriffFuerOrt(ziel.ort)),
|
|
97
98
|
zeiger: new ComposerZeiger(),
|
|
98
99
|
zustand,
|
|
@@ -855,12 +856,17 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
855
856
|
else
|
|
856
857
|
console.log(` liegen lassen Arbeitsverzeichnis ${pfad} (nicht mehr auf der Platte, ohne Bestätigung nicht vergessen)`);
|
|
857
858
|
}
|
|
859
|
+
// SOPS-Dateien, die verschlüsselt im Checkout lagen (E115).
|
|
860
|
+
const sopsQuellen = doctor.sops.length === 0 ? [] : await anwendungsfaelle.quellen();
|
|
861
|
+
for (const zeile of sopsBerichtZeilen(doctor.sops, sopsQuellen))
|
|
862
|
+
console.log(zeile);
|
|
858
863
|
// Die Sammelzeile steht ganz am Ende des Berichts (E109); „Außerdem" nur, wenn sonst etwas im Lauf stand.
|
|
859
864
|
const updateSammel = sammelZeile(updateOhne, !updateAndere &&
|
|
860
865
|
pluginFehler.length === 0 &&
|
|
861
866
|
bundleBericht(bundles).length === 0 &&
|
|
862
867
|
aufraeumen.verwaiste.length === 0 &&
|
|
863
|
-
doctor.fehlendeArbeitsverzeichnisse.length === 0
|
|
868
|
+
doctor.fehlendeArbeitsverzeichnisse.length === 0 &&
|
|
869
|
+
doctor.sops.length === 0);
|
|
864
870
|
if (updateSammel !== undefined)
|
|
865
871
|
console.log(updateSammel);
|
|
866
872
|
// Zuletzt hmgr selbst (E77): wer schon aktualisiert, will auch wissen, dass sein hmgr alt ist.
|
|
@@ -907,6 +913,15 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
907
913
|
const fehlend = await anwendungsfaelle.nichtVorhandeneHarnesses();
|
|
908
914
|
if (fehlend.length > 0)
|
|
909
915
|
console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
|
|
916
|
+
// SOPS-Dateien, die noch verschlüsselt im Checkout liegen (E115); ohne Fund keine Zeile.
|
|
917
|
+
const verschluesselt = await anwendungsfaelle.sopsVerschluesselteDateien();
|
|
918
|
+
if (verschluesselt.length > 0) {
|
|
919
|
+
const quellen = await anwendungsfaelle.quellen();
|
|
920
|
+
console.log('Noch verschlüsselt im Checkout:');
|
|
921
|
+
for (const { checkout, datei } of verschluesselt)
|
|
922
|
+
console.log(` ${datei} aus ${quellenAnzeigeName({ quelle: quelleAlsText(checkout.quelle), ref: checkout.ref }, quellen)} (${path.join(checkout.pfad, datei)})`);
|
|
923
|
+
console.log('hmgr doctor versucht, sie zu entschlüsseln, und nennt den Grund, wenn es nicht klappt.');
|
|
924
|
+
}
|
|
910
925
|
return 0;
|
|
911
926
|
}
|
|
912
927
|
if (kommando === 'log') {
|
|
@@ -936,7 +951,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
936
951
|
if (ereignis.art === 'checkout')
|
|
937
952
|
continue;
|
|
938
953
|
andere = true;
|
|
939
|
-
console.log(` ${verlaufEreignisZeile(ereignis)}`);
|
|
954
|
+
console.log(` ${verlaufEreignisZeile(ereignis, quellenListe)}`);
|
|
940
955
|
// Unter einem Versionssprung die Commits der neuen Version(en), eingerückt (E108).
|
|
941
956
|
if (ereignis.art === 'hmgr-installiert') {
|
|
942
957
|
const aenderungen = await anwendungsfaelle.hmgrAenderungen(ereignis.vorher, ereignis.version);
|
|
@@ -952,7 +967,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
952
967
|
}
|
|
953
968
|
if (kommando === 'doctor') {
|
|
954
969
|
restPruefen(schalterLesen(argv.slice(1), 'doctor').rest, 0, 'doctor');
|
|
955
|
-
const { fehlendeArbeitsverzeichnisse, vergessen } = await anwendungsfaelle.doctor();
|
|
970
|
+
const { fehlendeArbeitsverzeichnisse, vergessen, sops } = await anwendungsfaelle.doctor();
|
|
956
971
|
if (fehlendeArbeitsverzeichnisse.length === 0)
|
|
957
972
|
console.log('Keine fehlenden Arbeitsverzeichnisse.');
|
|
958
973
|
for (const pfad of fehlendeArbeitsverzeichnisse) {
|
|
@@ -961,6 +976,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
961
976
|
else
|
|
962
977
|
console.log(` liegen lassen ${pfad} (ohne Bestätigung nicht vergessen)`);
|
|
963
978
|
}
|
|
979
|
+
// SOPS-Dateien (E115): nur bei einem Fund eine Zeile, wer kein SOPS nutzt, sieht nichts davon.
|
|
980
|
+
const sopsQuellen = sops.length === 0 ? [] : await anwendungsfaelle.quellen();
|
|
981
|
+
for (const zeile of sopsBerichtZeilen(sops, sopsQuellen))
|
|
982
|
+
console.log(zeile);
|
|
964
983
|
return 0;
|
|
965
984
|
}
|
|
966
985
|
if (kommando === 'harness') {
|
|
@@ -1185,8 +1204,21 @@ function bundleBericht(eintraege) {
|
|
|
1185
1204
|
function aenderungsZeilen(aenderungen) {
|
|
1186
1205
|
return aenderungen.flatMap((a) => [` ${a.version}`, ...a.commits.map((c) => ` ${c}`)]);
|
|
1187
1206
|
}
|
|
1207
|
+
/** Bericht zu SOPS-Dateien aus doctor und update (E115), Spalten wie die übrigen Berichtszeilen. */
|
|
1208
|
+
function sopsBerichtZeilen(befunde, quellen) {
|
|
1209
|
+
return befunde.map((befund) => {
|
|
1210
|
+
const ort = `${befund.datei} aus ${quellenAnzeigeName({ quelle: quelleAlsText(befund.checkout.quelle), ref: befund.checkout.ref }, quellen)}`;
|
|
1211
|
+
if (befund.ergebnis === 'repariert')
|
|
1212
|
+
return ` entschlüsselt ${ort}`;
|
|
1213
|
+
if (befund.ergebnis === 'verschluesselt')
|
|
1214
|
+
return ` verschlüsselt ${ort}: ${sopsGrundText(befund.grund)}`;
|
|
1215
|
+
return ` fehlgeschlagen ${ort}: ${befund.fehler}`;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1188
1218
|
/** Eine Ereigniszeile im Verlauf (E80), eingerückt unter der Kopfzeile ihres Laufs. Checkouts stellt `checkoutGruppen` dar (E109). */
|
|
1189
|
-
function verlaufEreignisZeile(ereignis) {
|
|
1219
|
+
function verlaufEreignisZeile(ereignis, quellen) {
|
|
1220
|
+
if (ereignis.art === 'sops-verschluesselt' || ereignis.art === 'sops-repariert')
|
|
1221
|
+
return sopsZeile(ereignis, quellen);
|
|
1190
1222
|
if (ereignis.art === 'bundle') {
|
|
1191
1223
|
const teile = [
|
|
1192
1224
|
ereignis.gelegt.length > 0 ? `gelegt ${ereignis.gelegt.join(', ')}` : undefined,
|
|
@@ -59,3 +59,20 @@ export function verlaufZeitpunkt(iso, jetzt = new Date()) {
|
|
|
59
59
|
return `Gestern, ${uhr}`;
|
|
60
60
|
return `${zwei(datum.getDate())}.${zwei(datum.getMonth() + 1)}.${datum.getFullYear()}, ${uhr}`;
|
|
61
61
|
}
|
|
62
|
+
/** Warum eine SOPS-Datei verschlüsselt blieb (E115), in Alltagssprache. GUI: renderer.js, sopsGrundText. */
|
|
63
|
+
export function sopsGrundText(grund) {
|
|
64
|
+
if (grund === 'sops-fehlt')
|
|
65
|
+
return 'sops ist auf diesem Rechner nicht installiert';
|
|
66
|
+
if (grund === 'lokale-aenderung')
|
|
67
|
+
return 'die Datei ist im Checkout von Hand geändert';
|
|
68
|
+
if (grund === 'wrapper-fehlt')
|
|
69
|
+
return 'sops-clean.sh fehlt (weder in /usr/local/bin noch in ~/bin)';
|
|
70
|
+
return 'die Entschlüsselung klappt nicht, vermutlich fehlt der passende Schlüssel';
|
|
71
|
+
}
|
|
72
|
+
/** Verlaufszeile zu einer SOPS-Datei (E115): verschlüsselt geblieben oder nachträglich entschlüsselt. */
|
|
73
|
+
export function sopsZeile(ereignis, quellen) {
|
|
74
|
+
const ort = `${ereignis.datei} aus ${quellenAnzeigeName(ereignis, quellen)}`;
|
|
75
|
+
return ereignis.grund === undefined
|
|
76
|
+
? `Verschlüsselte Datei ${ort} entschlüsselt`
|
|
77
|
+
: `Datei ${ort} bleibt verschlüsselt: ${sopsGrundText(ereignis.grund)}`;
|
|
78
|
+
}
|
|
@@ -3202,9 +3202,25 @@
|
|
|
3202
3202
|
} else if (ereignis.art === 'cli-verfuegbar') {
|
|
3203
3203
|
li.appendChild(e('span', 'marke-ok', 'verfügbar'));
|
|
3204
3204
|
li.appendChild(e('span', 'pfad', 'CLI ' + ereignis.version));
|
|
3205
|
+
} else if (ereignis.art === 'sops-verschluesselt') {
|
|
3206
|
+
// E115: Wortlaut wie sopsZeile/sopsGrundText in src/cli/verlauf-anzeige.ts.
|
|
3207
|
+
li.appendChild(e('span', 'marke-aus', 'verschlüsselt'));
|
|
3208
|
+
li.appendChild(e('span', 'pfad', ereignis.datei + ' aus ' + verlaufQuellenName(ereignis.quelle, ereignis.ref)
|
|
3209
|
+
+ ' bleibt verschlüsselt: ' + sopsGrundText(ereignis.grund)));
|
|
3210
|
+
} else if (ereignis.art === 'sops-repariert') {
|
|
3211
|
+
li.appendChild(e('span', 'marke-ok', 'entschlüsselt'));
|
|
3212
|
+
li.appendChild(e('span', 'pfad', ereignis.datei + ' aus ' + verlaufQuellenName(ereignis.quelle, ereignis.ref)));
|
|
3205
3213
|
}
|
|
3206
3214
|
}
|
|
3207
3215
|
|
|
3216
|
+
/** Warum eine SOPS-Datei verschlüsselt blieb (E115), wie sopsGrundText in src/cli/verlauf-anzeige.ts. */
|
|
3217
|
+
function sopsGrundText(grund) {
|
|
3218
|
+
if (grund === 'sops-fehlt') return 'sops ist auf diesem Rechner nicht installiert';
|
|
3219
|
+
if (grund === 'lokale-aenderung') return 'die Datei ist im Checkout von Hand geändert';
|
|
3220
|
+
if (grund === 'wrapper-fehlt') return 'sops-clean.sh fehlt (weder in /usr/local/bin noch in ~/bin)';
|
|
3221
|
+
return 'die Entschlüsselung klappt nicht, vermutlich fehlt der passende Schlüssel';
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3208
3224
|
/**
|
|
3209
3225
|
* Anzeigename einer Quelle im Verlauf (E109). Baut quelleVerlaufName (src/kern/modell.ts) nach,
|
|
3210
3226
|
* der Renderer kann die Kern-Datei nicht importieren: Quelle aus QUELLEN über die Identität
|
|
@@ -1387,19 +1387,101 @@ export class Anwendungsfaelle {
|
|
|
1387
1387
|
* dieselbe Weiche wie bei `verwaisteAufraeumen`.
|
|
1388
1388
|
*/
|
|
1389
1389
|
async doctor(args) {
|
|
1390
|
-
const
|
|
1390
|
+
const ergebnis = await this.doctorPruefen(args?.rueckfragen ?? true);
|
|
1391
|
+
// Nur die SOPS-Befunde kommen in den Verlauf (E115); fehlende Arbeitsverzeichnisse meldet der
|
|
1392
|
+
// alleinstehende doctor wie bisher nur auf der Konsole.
|
|
1393
|
+
await this.sopsVermerken(args?.ausloeser ?? 'cli', ergebnis.sops, true);
|
|
1394
|
+
return ergebnis;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* doctor ohne Verlauf, damit der Aktualisieren-Lauf die Befunde in seinen eigenen Eintrag nimmt.
|
|
1398
|
+
* Dazu die SOPS-Prüfung (E115): Checkouts geführter Quellen, deren SOPS-Datei noch verschlüsselt
|
|
1399
|
+
* im Arbeitsbaum liegt, schreibt der Quellen-Port neu, wo die Probe klappt. Sie ist keine
|
|
1400
|
+
* Rückfrage wert: die Datei entspricht dem Commit, nur entschlüsselt.
|
|
1401
|
+
*/
|
|
1402
|
+
async doctorPruefen(rueckfragen) {
|
|
1391
1403
|
const zustand = await this.p.zustand.lesen();
|
|
1392
1404
|
const fehlend = [];
|
|
1393
1405
|
for (const pfad of zustand.arbeitsverzeichnisse)
|
|
1394
1406
|
if (!(await this.p.zeiger.verzeichnisExistiert(pfad)))
|
|
1395
1407
|
fehlend.push(pfad);
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1408
|
+
let vergessen = [];
|
|
1409
|
+
if (fehlend.length > 0 && rueckfragen && (await this.p.rueckfrage.bestaetigen(doctorFrage(fehlend)))) {
|
|
1410
|
+
for (const pfad of fehlend)
|
|
1411
|
+
await this.arbeitsverzeichnisVergessen(pfad);
|
|
1412
|
+
vergessen = [...fehlend];
|
|
1413
|
+
}
|
|
1414
|
+
const sops = [];
|
|
1415
|
+
for (const checkout of await this.gefuehrteCheckouts(zustand.quellen)) {
|
|
1416
|
+
const reparaturen = await this.p.quellen.sopsReparieren(checkout).catch(() => []);
|
|
1417
|
+
for (const reparatur of reparaturen)
|
|
1418
|
+
sops.push({ checkout, ...reparatur });
|
|
1419
|
+
}
|
|
1420
|
+
return { fehlendeArbeitsverzeichnisse: fehlend, vergessen, sops };
|
|
1421
|
+
}
|
|
1422
|
+
/** Checkouts, deren Quelle der Zustand führt (E54), mit dem Anzeigenamen der geführten Quelle. */
|
|
1423
|
+
async gefuehrteCheckouts(quellen) {
|
|
1424
|
+
const bekannt = new Map(quellen.map((q) => [quelleSchluessel(q), q]));
|
|
1425
|
+
const ergebnis = [];
|
|
1426
|
+
for (const checkout of await this.p.quellen.checkouts()) {
|
|
1427
|
+
const gefuehrt = bekannt.get(quelleSchluessel(checkout.quelle));
|
|
1428
|
+
if (gefuehrt !== undefined)
|
|
1429
|
+
ergebnis.push({ ...checkout, quelle: quelleMitNamen(checkout.quelle, gefuehrt.name) });
|
|
1430
|
+
}
|
|
1431
|
+
return ergebnis;
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* SOPS-Dateien, die im Arbeitsbaum eines Checkouts noch verschlüsselt liegen (E115), für
|
|
1435
|
+
* `hmgr status`. Liest nur, ruft kein sops auf und repariert nichts.
|
|
1436
|
+
*/
|
|
1437
|
+
async sopsVerschluesselteDateien() {
|
|
1438
|
+
const ergebnis = [];
|
|
1439
|
+
for (const checkout of await this.gefuehrteCheckouts((await this.p.zustand.lesen()).quellen)) {
|
|
1440
|
+
const dateien = await this.p.quellen.sopsDateien(checkout).catch(() => []);
|
|
1441
|
+
for (const d of dateien)
|
|
1442
|
+
if (d.verschluesselt)
|
|
1443
|
+
ergebnis.push({ checkout, datei: d.datei });
|
|
1444
|
+
}
|
|
1445
|
+
return ergebnis;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* SOPS-Befunde als Verlaufsereignisse (E115). „Verschlüsselt" und ein gescheitertes Neuschreiben
|
|
1449
|
+
* kommen einmal je Datei und Grund (`gemerkt.sops`), „repariert" jedes Mal, wenn es geschieht.
|
|
1450
|
+
* `vollstaendig`: die Befunde decken alle Checkouts ab, dann ersetzen sie die gemerkte Liste;
|
|
1451
|
+
* sonst (ein einzelner Checkout beim Verlinken) kommen sie dazu.
|
|
1452
|
+
*/
|
|
1453
|
+
async sopsEreignisse(befunde, vollstaendig) {
|
|
1454
|
+
const bisher = (await this.p.verlauf.lesen()).gemerkt.sops ?? [];
|
|
1455
|
+
const aktuell = [];
|
|
1456
|
+
const ereignisse = [];
|
|
1457
|
+
for (const befund of befunde) {
|
|
1458
|
+
const quelle = quelleAlsText(befund.checkout.quelle);
|
|
1459
|
+
const { ref } = befund.checkout;
|
|
1460
|
+
if (befund.ergebnis === 'repariert') {
|
|
1461
|
+
ereignisse.push({ art: 'sops-repariert', quelle, ref, datei: befund.datei });
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const art = befund.ergebnis === 'verschluesselt' ? befund.grund : 'fehlgeschlagen';
|
|
1465
|
+
const schluessel = `${quelle} @ ${ref}: ${befund.datei} | ${art}`;
|
|
1466
|
+
if (aktuell.includes(schluessel))
|
|
1467
|
+
continue;
|
|
1468
|
+
aktuell.push(schluessel);
|
|
1469
|
+
if (bisher.includes(schluessel))
|
|
1470
|
+
continue;
|
|
1471
|
+
if (befund.ergebnis === 'verschluesselt')
|
|
1472
|
+
ereignisse.push({ art: 'sops-verschluesselt', quelle, ref, datei: befund.datei, grund: befund.grund });
|
|
1473
|
+
else
|
|
1474
|
+
ereignisse.push({
|
|
1475
|
+
art: 'fehler',
|
|
1476
|
+
betrifft: `${quelle} @ ${ref}: ${befund.datei}`,
|
|
1477
|
+
fehler: `verschlüsselte Datei nicht neu geschrieben: ${befund.fehler}`,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return { ereignisse, gemerkt: vollstaendig ? aktuell : [...new Set([...bisher, ...aktuell])] };
|
|
1481
|
+
}
|
|
1482
|
+
async sopsVermerken(ausloeser, befunde, vollstaendig) {
|
|
1483
|
+
const { ereignisse, gemerkt } = await this.sopsEreignisse(befunde, vollstaendig);
|
|
1484
|
+
await this.verlaufVermerken(ausloeser, ereignisse, gemerkt);
|
|
1403
1485
|
}
|
|
1404
1486
|
/**
|
|
1405
1487
|
* Mehrere composer.json unter einem Verzeichnis (E26): hmgr rät nicht, es fragt. Bleibt die Frage
|
|
@@ -1480,7 +1562,11 @@ export class Anwendungsfaelle {
|
|
|
1480
1562
|
throw new OrtNichtUnterstuetzt(harness.name, ziel.ort.art);
|
|
1481
1563
|
const verzeichnis = harness.zielVerzeichnis(ziel.ort);
|
|
1482
1564
|
// Die Prüfung auf einen fremden Eintrag macht der Ziele-Port unmittelbar vor dem Anlegen (E12).
|
|
1483
|
-
|
|
1565
|
+
// Nur Quelle, Ref und Pfad gehen in den Link, die SOPS-Befunde nicht.
|
|
1566
|
+
const { sopsVerschluesselt, ...checkout } = await this.p.quellen.checkout(fund.quelle, fund.ref, [fund.skill]);
|
|
1567
|
+
// Verschlüsselt angekommene SOPS-Dateien (E115) gleich melden, nicht erst beim nächsten Lauf.
|
|
1568
|
+
if (sopsVerschluesselt.length > 0)
|
|
1569
|
+
await this.sopsVermerken('aktion', sopsVerschluesselt.map((v) => ({ checkout, datei: v.datei, ergebnis: 'verschluesselt', grund: v.grund })), false);
|
|
1484
1570
|
await harness.vorbereiten(ziel.ort);
|
|
1485
1571
|
await this.p.ziele.linkLegen(ziel, verzeichnis, fund.skill.name, checkout, fund.skill, ersetzen);
|
|
1486
1572
|
return { ziel, name: fund.skill.name, checkout, skill: fund.skill };
|
|
@@ -2076,9 +2162,15 @@ export class Anwendungsfaelle {
|
|
|
2076
2162
|
? { verwaiste: [], gefragt: false }
|
|
2077
2163
|
: await this.verwaisteAufraeumen(checkouts, rueckfragen);
|
|
2078
2164
|
const doctorErgebnis = nurSchluessel !== undefined
|
|
2079
|
-
? { fehlendeArbeitsverzeichnisse: [], vergessen: [] }
|
|
2080
|
-
: await this.
|
|
2081
|
-
|
|
2165
|
+
? { fehlendeArbeitsverzeichnisse: [], vergessen: [], sops: [] }
|
|
2166
|
+
: await this.doctorPruefen(rueckfragen);
|
|
2167
|
+
// Die SOPS-Befunde (E115) kommen in denselben Eintrag; ein eingeschränkter Lauf prüft sie nicht
|
|
2168
|
+
// und lässt die gemerkte Liste stehen.
|
|
2169
|
+
const sops = nurSchluessel !== undefined ? undefined : await this.sopsEreignisse(doctorErgebnis.sops, true);
|
|
2170
|
+
await this.verlaufVermerken(ausloeser, [
|
|
2171
|
+
...this.aktualisierungsEreignisse(checkouts, pluginFehler, bundles, aufraeumen, doctorErgebnis),
|
|
2172
|
+
...(sops?.ereignisse ?? []),
|
|
2173
|
+
], sops?.gemerkt);
|
|
2082
2174
|
return { checkouts, pluginHarnesses, pluginFehler, bundles, aufraeumen, doctor: doctorErgebnis };
|
|
2083
2175
|
}
|
|
2084
2176
|
/**
|
|
@@ -2129,13 +2221,23 @@ export class Anwendungsfaelle {
|
|
|
2129
2221
|
ereignisse.push({ art: 'arbeitsverzeichnis-vergessen', pfad });
|
|
2130
2222
|
return ereignisse;
|
|
2131
2223
|
}
|
|
2132
|
-
/**
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2224
|
+
/**
|
|
2225
|
+
* Neuen Eintrag vorn anfügen, nur wenn Ereignisse da sind (E80). Kappen übernimmt der Adapter.
|
|
2226
|
+
* `sopsGemerkt` (E115) ersetzt die gemerkten SOPS-Meldungen; hat sich nur diese Liste geändert,
|
|
2227
|
+
* wird sie ohne neuen Eintrag geschrieben.
|
|
2228
|
+
*/
|
|
2229
|
+
async verlaufVermerken(ausloeser, ereignisse, sopsGemerkt) {
|
|
2136
2230
|
const daten = await this.p.verlauf.lesen();
|
|
2231
|
+
const sopsGeaendert = sopsGemerkt !== undefined && JSON.stringify(sopsGemerkt) !== JSON.stringify(daten.gemerkt.sops ?? []);
|
|
2232
|
+
if (ereignisse.length === 0 && !sopsGeaendert)
|
|
2233
|
+
return;
|
|
2234
|
+
const gemerkt = sopsGeaendert ? { ...daten.gemerkt, sops: sopsGemerkt } : daten.gemerkt;
|
|
2235
|
+
if (ereignisse.length === 0) {
|
|
2236
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: daten.eintraege });
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2137
2239
|
const eintrag = { zeit: new Date().toISOString(), ausloeser, ereignisse };
|
|
2138
|
-
await this.p.verlauf.schreiben({ gemerkt
|
|
2240
|
+
await this.p.verlauf.schreiben({ gemerkt, eintraege: [eintrag, ...daten.eintraege] });
|
|
2139
2241
|
}
|
|
2140
2242
|
/** Der Verlauf, neueste zuerst (E80). */
|
|
2141
2243
|
async verlauf() {
|