hmgr 0.0.45 → 0.0.47
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/git.js +215 -10
- package/dist/adapter/sops.js +107 -0
- package/dist/adapter/zustand-json.js +2 -1
- package/dist/aenderungen.json +6 -0
- package/dist/cli/hilfe.js +44 -10
- package/dist/cli/main.js +67 -13
- package/dist/cli/verlauf-anzeige.js +17 -0
- package/dist/gui/main.js +80 -15
- package/dist/gui/preload.cjs +2 -0
- package/dist/gui/renderer/renderer.js +325 -88
- package/dist/gui/renderer/styles.css +8 -0
- package/dist/kern/anwendungsfaelle.js +250 -62
- package/package.json +1 -1
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
|
+
}
|
|
@@ -18,7 +18,8 @@ function deuten(roh) {
|
|
|
18
18
|
aktiveHarnesses: (daten.aktiveHarnesses ?? ['claude-code']),
|
|
19
19
|
// Fehlt das Feld (state.json vor E43), gilt: nichts ignoriert.
|
|
20
20
|
ignorierteEintraege: (daten.ignorierteEintraege ?? []),
|
|
21
|
-
// Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert.
|
|
21
|
+
// Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert. Ein Eintrag ohne
|
|
22
|
+
// `herausgenommen` (vor E114) hat keine herausgenommenen Mitglieder, das Feld bleibt dann weg.
|
|
22
23
|
bundleInstallationen: (daten.bundleInstallationen ?? []),
|
|
23
24
|
// Fehlt das Feld (state.json vor E78), gilt: jeder Harness hat nur sein Standard-Homedir.
|
|
24
25
|
harnessHeime: (daten.harnessHeime ?? []),
|
package/dist/aenderungen.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"fassung": 1,
|
|
3
3
|
"versionen": {
|
|
4
|
+
"0.0.47": [
|
|
5
|
+
"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."
|
|
6
|
+
],
|
|
7
|
+
"0.0.46": [
|
|
8
|
+
"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."
|
|
9
|
+
],
|
|
4
10
|
"0.0.45": [
|
|
5
11
|
"MCP-Server und die Listen in den Einstellungen (Harnesses, zusätzliche Homedirs) erscheinen jetzt alphabetisch sortiert, ebenso in hmgr mcp list."
|
|
6
12
|
],
|
package/dist/cli/hilfe.js
CHANGED
|
@@ -315,9 +315,13 @@ Liegt am Ziel schon derselbe Skill aus derselben Quelle als hmgr-Link, wird er a
|
|
|
315
315
|
übernommen: aus Bestand wird ein Bundle, ohne erst Links von Hand zu entfernen. Alles andere unter
|
|
316
316
|
demselben Namen ist ein Konflikt und wird genannt.
|
|
317
317
|
|
|
318
|
-
hmgr unlink <skill> auf ein einzelnes Mitglied
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
hmgr unlink <skill> auf ein einzelnes Mitglied nimmt es aus dem Bundle heraus: nur dieser Link
|
|
319
|
+
verschwindet, das Bundle bleibt im Ziel und merkt sich den Skill als herausgenommen. sync und
|
|
320
|
+
update legen ihn nicht wieder an, neue Mitglieder der Definition ergänzen sie weiter. Zurück kommt
|
|
321
|
+
er mit hmgr link <skill> desselben Skills aus derselben Quelle oder mit bundle sync --complete, das
|
|
322
|
+
alle fehlenden Mitglieder verlinkt. remove nimmt die noch verlinkten Mitglieder weg, auch wenn
|
|
323
|
+
schon welche herausgenommen sind. hmgr status und bundle list zeigen die Lage je Ziel, etwa
|
|
324
|
+
„4/5 … fehlt: x".
|
|
321
325
|
|
|
322
326
|
Den Abgleich macht schon hmgr update für alle installierten Bundles mit, es ist derselbe Vorgang
|
|
323
327
|
und dieselbe Ausgabe (E69): eine geänderte Definition kommt also mit dem gewöhnlichen
|
|
@@ -335,11 +339,14 @@ Schalter (add, remove und sync):
|
|
|
335
339
|
kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
|
|
336
340
|
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
337
341
|
--quelle <teil> nur bei add: Quelle eingrenzen, wenn den Namen mehrere Quellen führen
|
|
342
|
+
--complete nur bei sync: auch herausgenommene Mitglieder verlinken (Vervollständigen),
|
|
343
|
+
die Liste der herausgenommenen ist danach leer. Auch --vervollstaendigen
|
|
338
344
|
|
|
339
345
|
Beispiele:
|
|
340
346
|
hmgr bundle list
|
|
341
347
|
hmgr bundle add wordpress --ort dir:.
|
|
342
348
|
hmgr bundle sync
|
|
349
|
+
hmgr bundle sync wordpress --ort dir:. --complete
|
|
343
350
|
hmgr bundle remove wordpress --ort dir:.
|
|
344
351
|
|
|
345
352
|
Häufige Fälle:
|
|
@@ -524,7 +531,9 @@ ohne den übrigen Lauf aufzuhalten. hmgr bundle sync macht dasselbe für ein ein
|
|
|
524
531
|
Danach räumt update verwaiste Ref-Verzeichnisse auf: Checkouts, in die kein Link mehr zeigt, weil
|
|
525
532
|
ein Link inzwischen auf einen anderen Ref umgelegt wurde. Vor dem Löschen fragt hmgr einmal für
|
|
526
533
|
alle Kandidaten. Ohne Bestätigung, also auch ohne Terminal, bleibt alles liegen und wird nur
|
|
527
|
-
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).
|
|
528
537
|
|
|
529
538
|
So wird eine Änderung an einem Skill überall wirksam: in der Quelle committen und pushen, dann
|
|
530
539
|
hmgr update.
|
|
@@ -544,7 +553,8 @@ Eingeschränkter Lauf (E89): hmgr update <skill> zieht nur die Quelle nach, in d
|
|
|
544
553
|
von URL, Pfad oder Anzeigenamen der Quelle, Groß- und Kleinschreibung egal; passt keine oder
|
|
545
554
|
mehr als eine, bricht update mit der Liste ab. Beides zusammen geht nicht. Der Lauf umfasst die
|
|
546
555
|
Checkouts dieser Quelle und die Bundles, deren Definition in ihr liegt. Es entfallen: Plugins,
|
|
547
|
-
Aufräumen verwaister Checkouts,
|
|
556
|
+
Aufräumen verwaister Checkouts, die doctor-Prüfungen (fehlende Arbeitsverzeichnisse,
|
|
557
|
+
verschlüsselte SOPS-Dateien) sowie alle
|
|
548
558
|
übrigen Quellen und Bundles. Der Verlaufseintrag entsteht wie im vollen Lauf.
|
|
549
559
|
|
|
550
560
|
Schalter:
|
|
@@ -580,7 +590,10 @@ ausgecheckte Skills (E70). Quellen ohne Skill-Änderung sammelt eine Zeile am En
|
|
|
580
590
|
Ereignisse, je eine Zeile: ein nachgezogenes Bundle mit gelegten und entfernten Links, ein
|
|
581
591
|
aufgeräumter (gelöschter) Checkout, ein Fehler zu einer Quelle oder einem Bundle, dazu
|
|
582
592
|
hmgr-Ereignisse: ein Versionssprung von hmgr selbst (GUI oder CLI, erkannt beim Start bzw. bei
|
|
583
|
-
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
|
|
584
597
|
Zeit vor E109 zeigen nur „Geändert". Plugins stehen nicht im Verlauf (siehe hmgr help update).
|
|
585
598
|
|
|
586
599
|
Unter einem „installiert"-Eintrag von hmgr stehen die Änderungen: je Version seit der zuvor
|
|
@@ -604,8 +617,21 @@ Prüft, ob jedes gemerkte Arbeitsverzeichnis (hmgr dir add) noch auf der Platte
|
|
|
604
617
|
umbenanntes oder woanders neu geklontes Projekt hinterlässt sonst für immer einen toten Pfad im
|
|
605
618
|
Zustand, den nur hmgr dir forget von Hand entfernt. Bei einem Fund fragt doctor wie hmgr update
|
|
606
619
|
einmal für alle Funde, ob sie vergessen werden sollen; ohne Bestätigung bleiben sie gemeldet, aber
|
|
607
|
-
gemerkt. Anders als update holt doctor nichts aus dem Netz
|
|
608
|
-
|
|
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.
|
|
609
635
|
|
|
610
636
|
Schalter: keine.
|
|
611
637
|
|
|
@@ -615,6 +641,11 @@ Beispiel:
|
|
|
615
641
|
Häufige Fälle:
|
|
616
642
|
„liegen lassen … ohne Bestätigung nicht vergessen": die Frage kam ohne Ja zurück. In einem
|
|
617
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.
|
|
618
649
|
|
|
619
650
|
Siehe auch: hmgr help update, hmgr help dir`;
|
|
620
651
|
const STATUS = `hmgr status
|
|
@@ -624,7 +655,8 @@ und Ref, davor die Versionseinschränkung, falls der Zeiger eine nennt. Dahinter
|
|
|
624
655
|
abgeleitete Grund des Links: das Plugin, zu dem er gehört, oder „über Zeiger", von Hand gelegte
|
|
625
656
|
Links bleiben ohne Zusatz. Fremde Einträge und Links ohne bekannte Quelle sind eigens
|
|
626
657
|
gekennzeichnet, dazu kommen die Plugins des Ziels. Am Ende stehen die Harnesses, die auf diesem
|
|
627
|
-
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.
|
|
628
660
|
|
|
629
661
|
Schalter: keine.
|
|
630
662
|
|
|
@@ -637,10 +669,12 @@ Häufige Fälle:
|
|
|
637
669
|
Zustand steht. Mit hmgr source add <url> die Quelle wieder eintragen.
|
|
638
670
|
„Nicht vorhanden": der Harness hat sein Verzeichnis auf diesem Rechner nicht angelegt, hmgr
|
|
639
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.
|
|
640
674
|
Ein Ziel fehlt: Ziele inaktiver Harness-Typen zeigt status nicht, auch wenn dort noch Links
|
|
641
675
|
liegen. hmgr harness list zeigt den Stand, hmgr harness enable <name> holt die Ziele zurück.
|
|
642
676
|
|
|
643
|
-
Siehe auch: hmgr help adopt, hmgr help sync`;
|
|
677
|
+
Siehe auch: hmgr help adopt, hmgr help sync, hmgr help doctor`;
|
|
644
678
|
const HARNESS = `hmgr harness list
|
|
645
679
|
hmgr harness enable <name>
|
|
646
680
|
hmgr harness disable <name>
|