hmgr 0.0.6 → 0.0.7
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/bundle-yaml.js +87 -0
- package/dist/adapter/git.js +148 -0
- package/dist/adapter/paket-version.js +25 -0
- package/dist/adapter/zustand-json.js +9 -2
- package/dist/cli/hilfe.js +99 -0
- package/dist/cli/main.js +159 -5
- package/dist/gui/main.js +101 -0
- package/dist/gui/preload.cjs +4 -0
- package/dist/gui/renderer/index.html +12 -1
- package/dist/gui/renderer/renderer.js +415 -107
- package/dist/gui/renderer/styles.css +20 -0
- package/dist/kern/anwendungsfaelle.js +450 -9
- package/dist/kern/modell.js +80 -0
- package/dist/ports/zustand.js +15 -1
- package/package.json +20 -5
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Liest eine Bundle-Definition aus einer Quelle (E69). hmgr schreibt solche Dateien nie.
|
|
2
|
+
import { parseDocument } from 'yaml';
|
|
3
|
+
import { BundleDateiFehler } from "../kern/modell.js";
|
|
4
|
+
import { quelleAusText } from "./quelle-text.js";
|
|
5
|
+
/** Konvention für Bundle-Definitionen in einer Quelle (E69): `bundles/<name>.yaml`. */
|
|
6
|
+
export const BUNDLE_VERZEICHNIS = 'bundles';
|
|
7
|
+
/**
|
|
8
|
+
* Ob ein Pfad in der Quelle eine Bundle-Definition ist: eine Datei direkt unter `bundles/`, mit
|
|
9
|
+
* Endung `.yaml` oder `.yml`. Nichts Verschachteltes — die Konvention nennt genau eine Ebene, und
|
|
10
|
+
* ein Unterordner wäre eine stille zweite Bedeutung.
|
|
11
|
+
*/
|
|
12
|
+
export function istBundleDatei(pfad) {
|
|
13
|
+
return /^bundles\/[^/]+\.(?:yaml|yml)$/.test(pfad);
|
|
14
|
+
}
|
|
15
|
+
/** Der Dateiname ohne Verzeichnis und Endung: Ersatzname, wenn das Feld `name` fehlt. */
|
|
16
|
+
export function bundleDateiName(pfad) {
|
|
17
|
+
return pfad.replace(/^bundles\//, '').replace(/\.(?:yaml|yml)$/, '');
|
|
18
|
+
}
|
|
19
|
+
function fehlertext(fehler) {
|
|
20
|
+
return fehler instanceof Error ? fehler.message : String(fehler);
|
|
21
|
+
}
|
|
22
|
+
function istObjekt(wert) {
|
|
23
|
+
return typeof wert === 'object' && wert !== null && !Array.isArray(wert);
|
|
24
|
+
}
|
|
25
|
+
/** Erlaubte Felder auf oberster Ebene und je Mitglied. Alles andere ist ein Fehler mit Feldnamen. */
|
|
26
|
+
const FELDER = new Set(['name', 'description', 'skills']);
|
|
27
|
+
const MITGLIED_FELDER = new Set(['source', 'path']);
|
|
28
|
+
function mitgliedLesen(datei, roh, i) {
|
|
29
|
+
if (!istObjekt(roh))
|
|
30
|
+
throw new BundleDateiFehler(datei, `skills[${i}] muss ein Objekt mit source und path sein`);
|
|
31
|
+
for (const feld of Object.keys(roh))
|
|
32
|
+
if (!MITGLIED_FELDER.has(feld))
|
|
33
|
+
throw new BundleDateiFehler(datei, `skills[${i}]: unbekanntes Feld „${feld}"`);
|
|
34
|
+
const source = roh['source'];
|
|
35
|
+
if (typeof source !== 'string' || source.trim().length === 0)
|
|
36
|
+
throw new BundleDateiFehler(datei, `skills[${i}].source muss die Adresse einer Quelle sein (URL oder Pfad)`);
|
|
37
|
+
const pfad = roh['path'];
|
|
38
|
+
if (typeof pfad !== 'string' || pfad.trim().length === 0)
|
|
39
|
+
throw new BundleDateiFehler(datei, `skills[${i}].path muss der Pfad des Skills in der Quelle sein`);
|
|
40
|
+
// Führende und abschließende Schrägstriche fallen weg: `skills/x/` und `skills/x` meinen denselben
|
|
41
|
+
// Ordner, und der Katalog führt ihn ohne (E11). Ein Wurzel-Skill (leerer Pfad) ist nicht
|
|
42
|
+
// adressierbar — er hieße in der Definition „", das ist kein Text und fliegt oben schon auf.
|
|
43
|
+
return { quelle: quelleAusText(source.trim()), pfad: pfad.trim().replace(/^\/+/, '').replace(/\/+$/, '') };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Eine Bundle-Definition aus dem YAML-Text lesen und validieren (E69). `datei` ist der Pfad in der
|
|
47
|
+
* Quelle, er dient als Ersatzname (ohne Verzeichnis und Endung), wenn das Feld `name` fehlt: der
|
|
48
|
+
* Dateiname ist Konvention, das Feld ist die Aussage des Autors — dieselbe Rangfolge wie beim
|
|
49
|
+
* Skill-Namen (E32). `quelle` ist die Quelle, in der die Datei liegt.
|
|
50
|
+
*/
|
|
51
|
+
export function bundleLesen(text, datei, quelle) {
|
|
52
|
+
let daten;
|
|
53
|
+
try {
|
|
54
|
+
const dokument = parseDocument(text, { strict: true });
|
|
55
|
+
// `parseDocument` sammelt Syntaxfehler, statt zu werfen: ohne diese Prüfung käme eine kaputte
|
|
56
|
+
// Datei als halb gelesenes Objekt durch und der Nutzer bekäme eine Meldung über ein Feld statt
|
|
57
|
+
// über die Datei.
|
|
58
|
+
const erster = dokument.errors[0];
|
|
59
|
+
if (erster !== undefined)
|
|
60
|
+
throw new BundleDateiFehler(datei, `kaputtes YAML: ${erster.message}`);
|
|
61
|
+
daten = dokument.toJS();
|
|
62
|
+
}
|
|
63
|
+
catch (fehler) {
|
|
64
|
+
if (fehler instanceof BundleDateiFehler)
|
|
65
|
+
throw fehler;
|
|
66
|
+
throw new BundleDateiFehler(datei, `kaputtes YAML: ${fehlertext(fehler)}`);
|
|
67
|
+
}
|
|
68
|
+
if (!istObjekt(daten))
|
|
69
|
+
throw new BundleDateiFehler(datei, 'die Datei muss ein Objekt mit name, description und skills sein');
|
|
70
|
+
for (const feld of Object.keys(daten))
|
|
71
|
+
if (!FELDER.has(feld))
|
|
72
|
+
throw new BundleDateiFehler(datei, `unbekanntes Feld „${feld}"`);
|
|
73
|
+
const rohName = daten['name'];
|
|
74
|
+
if (rohName !== undefined && (typeof rohName !== 'string' || rohName.trim().length === 0))
|
|
75
|
+
throw new BundleDateiFehler(datei, 'name muss nicht leerer Text sein');
|
|
76
|
+
const name = typeof rohName === 'string' ? rohName.trim() : bundleDateiName(datei);
|
|
77
|
+
const beschreibung = daten['description'];
|
|
78
|
+
if (typeof beschreibung !== 'string' || beschreibung.trim().length === 0)
|
|
79
|
+
throw new BundleDateiFehler(datei, 'description muss nicht leerer Text sein');
|
|
80
|
+
const skills = daten['skills'];
|
|
81
|
+
if (!Array.isArray(skills))
|
|
82
|
+
throw new BundleDateiFehler(datei, 'skills muss eine Liste sein');
|
|
83
|
+
if (skills.length === 0)
|
|
84
|
+
throw new BundleDateiFehler(datei, 'skills darf nicht leer sein');
|
|
85
|
+
const mitglieder = skills.map((roh, i) => mitgliedLesen(datei, roh, i));
|
|
86
|
+
return { name, beschreibung: beschreibung.trim(), mitglieder, quelle, datei };
|
|
87
|
+
}
|
package/dist/adapter/git.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { readFile, readdir, mkdir, realpath, rm, stat } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { bundleLesen, istBundleDatei } from "./bundle-yaml.js";
|
|
5
6
|
import { marketplaceLesen } from "./marketplace-json.js";
|
|
6
7
|
import { SKILL_DATEI, skillAusPfad, skillName } from "./skill-md.js";
|
|
7
8
|
import { BASIS_NAME, basisPfad, quelleZerlegen, refDekodieren, worktreePfad } from "./checkout-pfad.js";
|
|
@@ -253,6 +254,55 @@ export class GitQuellen {
|
|
|
253
254
|
})),
|
|
254
255
|
};
|
|
255
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Inhalte beliebiger Dateien eines Refs in einem Aufruf, dasselbe Bündel-Muster wie
|
|
259
|
+
* `skillMdInhalte`: `git cat-file --batch` holt beim Partial Clone die fehlenden Blobs gebündelt.
|
|
260
|
+
* Was nicht lesbar ist, fehlt in der Karte.
|
|
261
|
+
*/
|
|
262
|
+
async dateiInhalte(basis, ref, pfade) {
|
|
263
|
+
if (pfade.length === 0)
|
|
264
|
+
return new Map();
|
|
265
|
+
const eingabe = `${pfade.map((p) => `${ref}:${p}`).join('\n')}\n`;
|
|
266
|
+
const ausgabe = await this.gitMitEingabe(['cat-file', '--batch'], basis, eingabe).catch(() => undefined);
|
|
267
|
+
if (ausgabe === undefined)
|
|
268
|
+
return new Map();
|
|
269
|
+
return batchInhalte(ausgabe, pfade);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Bundle-Definitionen der Quelle (E69): jede Datei unter `bundles/`, gelesen wie der Marketplace
|
|
273
|
+
* über den Git-Baum statt über einen Checkout. Eine kaputte Datei ergibt einen Fehler-Eintrag,
|
|
274
|
+
* keinen Abbruch: die übrigen Bundles derselben Quelle bleiben nutzbar.
|
|
275
|
+
*/
|
|
276
|
+
async bundles(quelle, ref) {
|
|
277
|
+
let dateien;
|
|
278
|
+
let inhalte;
|
|
279
|
+
if (quelle.art === 'lokal') {
|
|
280
|
+
dateien = (await dateienAufPlatte(quelle.pfad)).filter(istBundleDatei).sort();
|
|
281
|
+
inhalte = new Map();
|
|
282
|
+
for (const datei of dateien) {
|
|
283
|
+
const roh = await readFile(path.join(quelle.pfad, datei), 'utf8').catch(() => undefined);
|
|
284
|
+
if (roh !== undefined)
|
|
285
|
+
inhalte.set(datei, roh);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const basis = await this.basis(quelle);
|
|
290
|
+
const ausgabe = await this.git(['ls-tree', '-r', '--name-only', ref], basis);
|
|
291
|
+
dateien = ausgabe.split('\n').filter(istBundleDatei).sort();
|
|
292
|
+
inhalte = await this.dateiInhalte(basis, ref, dateien);
|
|
293
|
+
}
|
|
294
|
+
return dateien.map((datei) => {
|
|
295
|
+
const inhalt = inhalte.get(datei);
|
|
296
|
+
if (inhalt === undefined)
|
|
297
|
+
return { art: 'fehler', datei, fehler: 'Datei nicht lesbar' };
|
|
298
|
+
try {
|
|
299
|
+
return { art: 'bundle', datei, bundle: bundleLesen(inhalt, datei, quelle) };
|
|
300
|
+
}
|
|
301
|
+
catch (fehler) {
|
|
302
|
+
return { art: 'fehler', datei, fehler: fehler instanceof Error ? fehler.message : String(fehler) };
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
256
306
|
async refs(quelle) {
|
|
257
307
|
if (quelle.art === 'lokal')
|
|
258
308
|
return [LOKAL_REF];
|
|
@@ -374,6 +424,104 @@ export class GitQuellen {
|
|
|
374
424
|
await this.git(['checkout', '--detach', checkout.ref], checkout.pfad);
|
|
375
425
|
return { ergebnis: 'aktualisiert', von, nach };
|
|
376
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Geänderte Pfade zwischen zwei Commits (E70). `--no-renames` aus zwei Gründen: eine Umbenennung
|
|
429
|
+
* soll hier als Änderung an beiden Pfaden zählen, und die Ähnlichkeitsprüfung bräuchte die Blobs,
|
|
430
|
+
* die ein Partial Clone (`--filter=blob:none`) erst aus dem Netz holen müsste. Eine lokale Quelle
|
|
431
|
+
* hat keinen Checkout und damit keine zwei Commits (B11), sie kommt hier nie an.
|
|
432
|
+
*/
|
|
433
|
+
async geaenderteDateien(quelle, von, nach) {
|
|
434
|
+
if (quelle.art === 'lokal')
|
|
435
|
+
return [];
|
|
436
|
+
try {
|
|
437
|
+
const basis = await this.basis(quelle);
|
|
438
|
+
const ausgabe = await this.git(['diff', '--name-only', '--no-renames', von, nach], basis);
|
|
439
|
+
return ausgabe.split('\n').filter((z) => z.length > 0);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Skills, die in diesem Checkout auf der Platte liegen (E70): dieselbe Regel wie bei einer
|
|
447
|
+
* lokalen Quelle, nur auf das Worktree-Verzeichnis angewandt. Der Sparse-Checkout hat dort nur
|
|
448
|
+
* die gewählten Skills materialisiert (E22), also nennt der Update-Bericht auch nur diese.
|
|
449
|
+
*/
|
|
450
|
+
async ausgecheckteSkills(checkout) {
|
|
451
|
+
const pfade = skillPfadeAusDateien(await dateienAufPlatte(checkout.pfad));
|
|
452
|
+
const inhalte = await inhalteAufPlatte(checkout.pfad, pfade);
|
|
453
|
+
const ersatzname = ersatznameDerQuelle(checkout.quelle);
|
|
454
|
+
return pfade.map((pfad) => skillAusPfad(pfad, inhalte.get(pfad), ersatzname));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Ein `git log --name-only` je Quelle statt eines Aufrufs je Skill (E70). Die Ausgabe wird
|
|
458
|
+
* gelesen, während sie entsteht, und der Lauf abgebrochen, sobald jeder gesuchte Pfad sein Datum
|
|
459
|
+
* hat: der jüngste Commit steht vorn, alles Ältere ist unnötig. Gemessen an einem Repo mit 20
|
|
460
|
+
* Skills und 401 Commits: 6 ms gegen 110 ms für 20 einzelne `git log -1`.
|
|
461
|
+
*
|
|
462
|
+
* Format: `%x00%cI` stellt jeder Datumszeile ein NUL voran, das in keinem Dateipfad vorkommt —
|
|
463
|
+
* daran trennen sich Kopf und Dateiliste. `--no-renames` hält die Ähnlichkeitsprüfung heraus, die
|
|
464
|
+
* beim Partial Clone Blobs nachladen würde. Scheitert der Aufruf (kein Repo, unbekannter Ref),
|
|
465
|
+
* bleibt die Karte leer, das ist kein Fehler.
|
|
466
|
+
*/
|
|
467
|
+
async logDaten(cwd, ref, pfade) {
|
|
468
|
+
const daten = new Map();
|
|
469
|
+
const offen = new Set(pfade);
|
|
470
|
+
if (offen.size === 0)
|
|
471
|
+
return daten;
|
|
472
|
+
return await new Promise((erfuellen) => {
|
|
473
|
+
const kind = spawn('git', ['log', '--format=%x00%cI', '--name-only', '--no-renames', ref], {
|
|
474
|
+
cwd,
|
|
475
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
476
|
+
timeout: 300_000,
|
|
477
|
+
killSignal: 'SIGKILL',
|
|
478
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
479
|
+
});
|
|
480
|
+
let rest = '';
|
|
481
|
+
let datum;
|
|
482
|
+
const fertig = () => erfuellen(daten);
|
|
483
|
+
kind.on('error', fertig);
|
|
484
|
+
kind.on('close', fertig);
|
|
485
|
+
// Nach dem Abbruch schreibt git in eine Pipe, die niemand mehr liest: der Fehler darf den
|
|
486
|
+
// Prozess nicht mitnehmen, das Ergebnis steht da schon fest.
|
|
487
|
+
kind.stdout.on('error', () => { });
|
|
488
|
+
kind.stdout.on('data', (teil) => {
|
|
489
|
+
rest += teil.toString('utf8');
|
|
490
|
+
const zeilen = rest.split('\n');
|
|
491
|
+
rest = zeilen.pop() ?? '';
|
|
492
|
+
for (const zeile of zeilen) {
|
|
493
|
+
if (zeile.startsWith('\0')) {
|
|
494
|
+
datum = zeile.slice(1);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (zeile.length === 0 || datum === undefined)
|
|
498
|
+
continue;
|
|
499
|
+
for (const pfad of [...offen])
|
|
500
|
+
// Ein Wurzel-Skill (Pfad `''`, E11-Nachtrag) ist das ganze Repo: jede Datei zählt.
|
|
501
|
+
if (pfad === '' || zeile === pfad || zeile.startsWith(`${pfad}/`)) {
|
|
502
|
+
daten.set(pfad, datum);
|
|
503
|
+
offen.delete(pfad);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (offen.size === 0)
|
|
507
|
+
kind.kill('SIGTERM');
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Änderungsdatum je Skill (E70, F70=O1). Eine lokale Quelle hat keinen Basis-Clone, wohl aber
|
|
513
|
+
* meist eine Arbeitskopie (B11): dort zählt der HEAD-Stand auf der Platte, wie überall sonst bei
|
|
514
|
+
* lokalen Quellen. Ist der Pfad kein Git-Repo, bleibt die Karte leer und der Katalog nennt für
|
|
515
|
+
* diese Quelle kein Datum.
|
|
516
|
+
*/
|
|
517
|
+
async letzteAenderungen(quelle, ref, pfade) {
|
|
518
|
+
if (quelle.art === 'lokal')
|
|
519
|
+
return await this.logDaten(quelle.pfad, 'HEAD', pfade);
|
|
520
|
+
const basis = await this.basis(quelle).catch(() => undefined);
|
|
521
|
+
if (basis === undefined)
|
|
522
|
+
return new Map();
|
|
523
|
+
return await this.logDaten(basis, ref, pfade);
|
|
524
|
+
}
|
|
377
525
|
/**
|
|
378
526
|
* Checkout von der Platte nehmen. Ein Worktree steht im Basis-Clone eingetragen, ein blankes
|
|
379
527
|
* Löschen des Verzeichnisses ließe den Eintrag stehen und ein späterer `worktree add` auf
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Die Version des Pakets, gelesen aus seiner package.json (E72). Keine Kopie im Code: eine
|
|
2
|
+
// zweite Stelle mit derselben Zahl geht früher oder später auseinander.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
/**
|
|
5
|
+
* Die package.json liegt in jeder Lage zwei Ebenen über diesem Modul: in der Quelle
|
|
6
|
+
* `src/adapter/` → Wurzel, im Build `dist/adapter/` → Wurzel, in der npm-Installation
|
|
7
|
+
* `node_modules/hmgr/dist/adapter/` → `node_modules/hmgr/`, im gepackten App-Bundle
|
|
8
|
+
* `app.asar/dist/adapter/` → `app.asar/`. Aufgelöst über `import.meta.url`, damit weder das
|
|
9
|
+
* Arbeitsverzeichnis noch der Aufrufweg hineinspielt.
|
|
10
|
+
*
|
|
11
|
+
* Bewusst kein Import mit `with { type: 'json' }`: der zöge die package.json beim Bauen nach
|
|
12
|
+
* `dist` und legte damit eine zweite Wahrheit an.
|
|
13
|
+
*/
|
|
14
|
+
const PAKET_JSON = new URL('../../package.json', import.meta.url);
|
|
15
|
+
let gemerkt;
|
|
16
|
+
/** Die Versionsnummer aus der package.json des Pakets, etwa „0.0.6". */
|
|
17
|
+
export function paketVersion() {
|
|
18
|
+
if (gemerkt === undefined) {
|
|
19
|
+
const inhalt = JSON.parse(readFileSync(PAKET_JSON, 'utf8'));
|
|
20
|
+
if (typeof inhalt.version !== 'string')
|
|
21
|
+
throw new Error(`Die package.json unter ${PAKET_JSON.pathname} nennt keine Version.`);
|
|
22
|
+
gemerkt = inhalt.version;
|
|
23
|
+
}
|
|
24
|
+
return gemerkt;
|
|
25
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Lokaler Zustand als eine JSON-Datei, Standard ~/.hmgr/state.json (E29).
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { leererZustand } from "../ports/zustand.js";
|
|
4
|
+
import { ZUSTAND_FASSUNG, leererZustand, } from "../ports/zustand.js";
|
|
5
5
|
import { ohneBom } from "./datei-text.js";
|
|
6
6
|
export class ZustandJson {
|
|
7
7
|
pfad;
|
|
@@ -14,6 +14,9 @@ export class ZustandJson {
|
|
|
14
14
|
return leererZustand();
|
|
15
15
|
const daten = JSON.parse(ohneBom(roh));
|
|
16
16
|
return {
|
|
17
|
+
// Fehlt das Feld (state.json vor E69), ist es die erste Fassung: alles ohne Bundles.
|
|
18
|
+
// Geschrieben wird danach in der aktuellen Fassung, ein Rückweg ist nicht vorgesehen.
|
|
19
|
+
fassung: typeof daten.fassung === 'number' ? daten.fassung : 1,
|
|
17
20
|
quellen: (daten.quellen ?? []),
|
|
18
21
|
arbeitsverzeichnisse: (daten.arbeitsverzeichnisse ?? []),
|
|
19
22
|
bindungen: (daten.bindungen ?? []),
|
|
@@ -21,10 +24,14 @@ export class ZustandJson {
|
|
|
21
24
|
aktiveHarnesses: (daten.aktiveHarnesses ?? ['claude-code']),
|
|
22
25
|
// Fehlt das Feld (state.json vor E43), gilt: nichts ignoriert.
|
|
23
26
|
ignorierteEintraege: (daten.ignorierteEintraege ?? []),
|
|
27
|
+
// Fehlt das Feld (state.json vor E69), gilt: kein Bundle installiert.
|
|
28
|
+
bundleInstallationen: (daten.bundleInstallationen ?? []),
|
|
24
29
|
};
|
|
25
30
|
}
|
|
26
31
|
async schreiben(zustand) {
|
|
27
32
|
await mkdir(path.dirname(this.pfad), { recursive: true });
|
|
28
|
-
|
|
33
|
+
// Immer in der aktuellen Fassung, egal welche gelesen wurde: das Lesen hat den Bestand schon
|
|
34
|
+
// auf die aktuelle Form gebracht, ein alter Vermerk in der Datei wäre danach eine Lüge.
|
|
35
|
+
await writeFile(this.pfad, `${JSON.stringify({ ...zustand, fassung: ZUSTAND_FASSUNG }, null, 2)}\n`);
|
|
29
36
|
}
|
|
30
37
|
}
|
package/dist/cli/hilfe.js
CHANGED
|
@@ -22,6 +22,10 @@ hmgr <kommando> [schalter]
|
|
|
22
22
|
plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
|
|
23
23
|
plugin remove <name> Plugin aus einem Ziel entfernen
|
|
24
24
|
plugin list Plugins der Marketplaces aller Quellen
|
|
25
|
+
bundle add <name> Bundle als Ganzes in ein Ziel installieren
|
|
26
|
+
bundle remove <name> Bundle als Ganzes aus einem Ziel entfernen
|
|
27
|
+
bundle sync [name] Installierte Bundles gegen ihre Definition ziehen
|
|
28
|
+
bundle list Bundles der bekannten Quellen
|
|
25
29
|
mcp add <name> MCP-Server aus dem Katalog in ein Ziel einhängen
|
|
26
30
|
mcp remove <name> MCP-Server aus einem Ziel aushängen
|
|
27
31
|
mcp migrate <name> MCP-Server aus der geteilten .mcp.json in den privaten Scope übernehmen
|
|
@@ -33,6 +37,7 @@ hmgr <kommando> [schalter]
|
|
|
33
37
|
harness enable <name> Harness-Typ aktivieren, hmgr richtet ihn künftig ein
|
|
34
38
|
harness disable <name> Harness-Typ deaktivieren, mit Angebot zum Säubern
|
|
35
39
|
harness skills-dir <name> Verwalteten Skill-Ordner zeigen, festlegen oder entfernen
|
|
40
|
+
--version Versionsnummer von hmgr ausgeben
|
|
36
41
|
|
|
37
42
|
hmgr help <kommando> zeigt Schalter, Beispiele und Zusammenhänge.`;
|
|
38
43
|
const SOURCE = `hmgr source add <url|pfad> [--name <name>]
|
|
@@ -230,6 +235,87 @@ Häufige Fälle:
|
|
|
230
235
|
kann nur die Quelle beheben.
|
|
231
236
|
|
|
232
237
|
Siehe auch: hmgr help source, hmgr help sync`;
|
|
238
|
+
const BUNDLE = `hmgr bundle add <name> [schalter]
|
|
239
|
+
hmgr bundle remove <name> [schalter]
|
|
240
|
+
hmgr bundle sync [name] [schalter]
|
|
241
|
+
hmgr bundle list
|
|
242
|
+
|
|
243
|
+
Ein Bundle ist eine benannte Zusammenstellung von Skills über Quellen hinweg: was für ein Vorhaben
|
|
244
|
+
zusammengehört, liegt selten in einem Repo. Anders als ein Plugin (das lebt in einem Marketplace und
|
|
245
|
+
löst Abhängigkeiten nur dort auf) darf ein Bundle Skills aus beliebig vielen Quellen nennen.
|
|
246
|
+
|
|
247
|
+
Definiert wird es von Hand, als YAML-Datei in einer Quelle, Konvention bundles/<name>.yaml. hmgr
|
|
248
|
+
liest und validiert sie, schreibt sie nie — über die Quelle kommt sie versioniert auf alle
|
|
249
|
+
Maschinen. Es gibt keinen eigenen Katalog und keinen Pflegebefehl: bundle list zeigt, was beim
|
|
250
|
+
Quellen-Scan gefunden wurde.
|
|
251
|
+
|
|
252
|
+
Format der Datei:
|
|
253
|
+
|
|
254
|
+
name optional, sonst gilt der Dateiname ohne Endung
|
|
255
|
+
description Pflicht, kurzer Text
|
|
256
|
+
skills Pflicht, nicht leere Liste; je Eintrag source (URL oder Pfad der Quelle)
|
|
257
|
+
und path (Pfad des Skill-Ordners in dieser Quelle)
|
|
258
|
+
|
|
259
|
+
Beispiel bundles/wordpress.yaml:
|
|
260
|
+
|
|
261
|
+
name: wordpress
|
|
262
|
+
description: Alles für die WordPress-Arbeit
|
|
263
|
+
skills:
|
|
264
|
+
- source: git@gitea.example.com:tim/skills.git
|
|
265
|
+
path: skills/wp-core
|
|
266
|
+
- source: https://github.com/taquiri/skills
|
|
267
|
+
path: skills/tq-mailform
|
|
268
|
+
|
|
269
|
+
Ein Ref je Mitglied kennt die Definition nicht: der Link folgt dem Ref, mit dem die Quelle
|
|
270
|
+
eingerichtet ist. Angesprochen wird der Skill über seinen Pfad, nicht über seinen Namen — der Name
|
|
271
|
+
steht im Frontmatter und wird erst beim Lesen der Quelle bekannt.
|
|
272
|
+
|
|
273
|
+
Je Ziel ist das Bundle eine Einheit: add legt alle seine Links, remove nimmt alle wieder weg, sync
|
|
274
|
+
zieht Änderungen an der Definition nach. Nennt die Definition eine Quelle, die hmgr noch nicht
|
|
275
|
+
kennt, legt add sie an und sagt, welche neu sind. Lässt sich auch nur ein Mitglied nicht auflösen
|
|
276
|
+
oder liegt am Ziel ein Namenskonflikt, bricht add ab, bevor irgendetwas verlinkt oder eine Quelle
|
|
277
|
+
angelegt ist — ein halbes Bundle wäre stilles Fehlen von Skills. Wer nur einen Teil braucht,
|
|
278
|
+
verlinkt die erreichbaren Skills einzeln mit hmgr link.
|
|
279
|
+
|
|
280
|
+
Liegt am Ziel schon derselbe Skill aus derselben Quelle als hmgr-Link, wird er als Mitglied
|
|
281
|
+
übernommen: aus Bestand wird ein Bundle, ohne erst Links von Hand zu entfernen. Alles andere unter
|
|
282
|
+
demselben Namen ist ein Konflikt und wird genannt.
|
|
283
|
+
|
|
284
|
+
hmgr unlink <skill> auf ein einzelnes Mitglied zerbricht das Bundle in diesem Ziel: die übrigen
|
|
285
|
+
Links bleiben als einzelne Links liegen, der Bundle-Verweis fällt weg. Das ist kein Fehler, hmgr
|
|
286
|
+
sagt es nur.
|
|
287
|
+
|
|
288
|
+
Den Abgleich macht schon hmgr update für alle installierten Bundles mit, es ist derselbe Vorgang
|
|
289
|
+
und dieselbe Ausgabe (E69): eine geänderte Definition kommt also mit dem gewöhnlichen
|
|
290
|
+
Aktualisieren an, ohne dass man von der Änderung wissen müsste. bundle sync ist der gezielte Weg
|
|
291
|
+
daneben, wenn nur ein Bundle oder nur ein Ziel gemeint ist; ohne Namen und ohne Ziel-Schalter geht
|
|
292
|
+
es denselben Bestand durch wie update. Was sich nicht vollständig nachziehen lässt, bleibt in
|
|
293
|
+
seinem Ziel auf dem alten Stand und wird als fehlgeschlagen gemeldet.
|
|
294
|
+
|
|
295
|
+
Der Zeiger bleibt außen vor: die Zugehörigkeit der Links führt hmgr in seinem Zustand, nicht in der
|
|
296
|
+
composer.json. --local gibt es deshalb nicht, und die Sperre aus E57 trifft bundle nie.
|
|
297
|
+
|
|
298
|
+
Schalter (add, remove und sync):
|
|
299
|
+
--harness <name> claude-code, hermes oder agents, Standard claude-code
|
|
300
|
+
--ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
|
|
301
|
+
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
302
|
+
--quelle <teil> nur bei add: Quelle eingrenzen, wenn den Namen mehrere Quellen führen
|
|
303
|
+
|
|
304
|
+
Beispiele:
|
|
305
|
+
hmgr bundle list
|
|
306
|
+
hmgr bundle add wordpress --ort dir:.
|
|
307
|
+
hmgr bundle sync
|
|
308
|
+
hmgr bundle remove wordpress --ort dir:.
|
|
309
|
+
|
|
310
|
+
Häufige Fälle:
|
|
311
|
+
„steht in keiner Quelle": hmgr bundle list zeigt die bekannten Namen, sonst erst hmgr source add.
|
|
312
|
+
„liegt in mehreren Quellen": mit --quelle <teilstring> entscheiden.
|
|
313
|
+
„ist auf dieser Maschine nicht vollständig herstellbar": die Meldung nennt je Mitglied den Grund,
|
|
314
|
+
meist eine Quelle ohne Zugriff oder ein Pfad, an dem kein Skill (mehr) liegt.
|
|
315
|
+
„lässt sich in … nicht anlegen": die Meldung listet alle Namen, die am Ziel schon belegt sind.
|
|
316
|
+
„kein Bundle … installiert": hmgr status zeigt je Ziel seine Bundles.
|
|
317
|
+
|
|
318
|
+
Siehe auch: hmgr help link, hmgr help plugin, hmgr help status`;
|
|
233
319
|
const MCP = `hmgr mcp add <name> [schalter]
|
|
234
320
|
hmgr mcp remove <name> [schalter]
|
|
235
321
|
hmgr mcp migrate <name> [schalter]
|
|
@@ -353,6 +439,18 @@ Plugin-Fähigkeit deren Plugins nach (E14). Die Links zeigen weiter auf dieselbe
|
|
|
353
439
|
nicht erneuert werden. Ein Checkout mit lokaler Änderung wird übersprungen und gemeldet (E15),
|
|
354
440
|
damit nichts verloren geht. Lokale Quellen haben keinen Checkout und bleiben unberührt.
|
|
355
441
|
|
|
442
|
+
Unter jeder „aktualisiert"-Zeile steht, welche Skills der neue Stand geändert hat (E70): die Namen
|
|
443
|
+
kommen aus dem Vergleich von altem und neuem Commit, gezählt werden nur Skills, die in diesem
|
|
444
|
+
Checkout auch ausgecheckt sind. Berührt ein Commit nur Dateien daneben (README, CI), bleibt die
|
|
445
|
+
Zeile weg.
|
|
446
|
+
|
|
447
|
+
Im selben Lauf gleicht update die installierten Bundles gegen ihre Definitionen ab (E69): eine
|
|
448
|
+
Definition, die inzwischen ein Mitglied mehr oder weniger nennt, wird in jedem Ziel nachgezogen, in
|
|
449
|
+
dem das Bundle hängt. Das ist der Weg, auf dem eine Bundle-Änderung aus einer Quelle ankommt — ohne
|
|
450
|
+
ihn müsste man von seinen eigenen Änderungen wissen, um sie zu holen. Was sich nicht vollständig
|
|
451
|
+
nachziehen lässt, bleibt in seinem Ziel auf dem alten Stand und wird als fehlgeschlagen gemeldet,
|
|
452
|
+
ohne den übrigen Lauf aufzuhalten. hmgr bundle sync macht dasselbe für ein einzelnes Bundle.
|
|
453
|
+
|
|
356
454
|
Danach räumt update verwaiste Ref-Verzeichnisse auf: Checkouts, in die kein Link mehr zeigt, weil
|
|
357
455
|
ein Link inzwischen auf einen anderen Ref umgelegt wurde. Vor dem Löschen fragt hmgr einmal für
|
|
358
456
|
alle Kandidaten. Ohne Bestätigung, also auch ohne Terminal, bleibt alles liegen und wird nur
|
|
@@ -456,6 +554,7 @@ const THEMEN = new Map([
|
|
|
456
554
|
['unlink', UNLINK],
|
|
457
555
|
['adopt', ADOPT],
|
|
458
556
|
['plugin', PLUGIN],
|
|
557
|
+
['bundle', BUNDLE],
|
|
459
558
|
['mcp', MCP],
|
|
460
559
|
['sync', SYNC],
|
|
461
560
|
['update', UPDATE],
|