hmgr 0.0.50 → 0.0.52

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.
@@ -51,6 +51,9 @@ export class Agents {
51
51
  if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
52
52
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
53
53
  }
54
+ quellOrdner() {
55
+ return `${AGENTS_VERZEICHNIS}/${SKILLS_ORDNER}`;
56
+ }
54
57
  folgtLinks() {
55
58
  return true;
56
59
  }
@@ -681,6 +681,9 @@ export class ClaudeCode {
681
681
  if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
682
682
  await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
683
683
  }
684
+ quellOrdner() {
685
+ return `${CLAUDE_VERZEICHNIS}/${SKILLS_ORDNER}`;
686
+ }
684
687
  folgtLinks() {
685
688
  return true;
686
689
  }
@@ -289,6 +289,9 @@ export class Hermes {
289
289
  await mkdir(hermesHome, { recursive: true });
290
290
  await writeFile(pfad, doc.toString());
291
291
  }
292
+ quellOrdner() {
293
+ return `${HERMES_VERZEICHNIS}/${EIGENER_ORDNER}`;
294
+ }
292
295
  folgtLinks() {
293
296
  return true;
294
297
  }
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "fassung": 1,
3
3
  "versionen": {
4
+ "0.0.52": [
5
+ "Liegt ein Skill in einem Repo in mehreren Fassungen, je Werkzeug eine (etwa `.claude/skills` und `.hermes/skills`), verlinkt hmgr jetzt für jeden Harness die passende Fassung. Gibt es keine passende, nennt die Meldung die Pfade, und `hmgr link … --pfad` wählt einen aus."
6
+ ],
7
+ "0.0.51": [
8
+ "Ein gelöschter Projektordner, an dem noch ein Bundle hing, wird beim Aktualisieren nicht mehr neu angelegt. `hmgr doctor` findet solche Ordner jetzt und bietet an, sie samt ihren Bundles zu vergessen."
9
+ ],
4
10
  "0.0.50": [
5
11
  "Die Suche in hmgr findet jetzt auch Ordner und Skills mit Umlauten, etwa „döring“. Unter macOS legt hmgr denselben Ordner nicht mehr doppelt an, wenn er einmal aus dem Finder und einmal getippt hinzugefügt wird."
6
12
  ],
package/dist/cli/hilfe.js CHANGED
@@ -147,6 +147,7 @@ Schalter:
147
147
  --ort <ort> home, wsl:<distro>, dir:<pfad> oder profil:<name>, Standard home. profil
148
148
  kennt nur hermes, wsl:<distro> nur claude-code unter Windows (E78)
149
149
  --quelle <teil> Teilstring der Quelle, wenn der Skill-Name in mehreren Quellen vorkommt
150
+ --pfad <skill-pfad> Skill-Ordner in der Quelle, wenn der Name dort mehrfach vorkommt (E121)
150
151
  --version <spanne> Versionseinschränkung nach Composer-Regeln (etwa ^1.2) oder ein Tag- oder
151
152
  Branchname. Nur mit Zeiger, also dir:<pfad> ohne --local (E33)
152
153
  --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
@@ -155,11 +156,15 @@ Schalter:
155
156
 
156
157
  Beispiele:
157
158
  hmgr link netzwerk
159
+ hmgr link impeccable --pfad plugin/skills/impeccable
158
160
  hmgr link netzwerk --harness hermes --ort profil:researcher
159
161
  hmgr link tq-schema --ort dir:. --version ^2.0
160
162
 
161
163
  Häufige Fälle:
162
164
  „liegt in mehreren Quellen": mit --quelle <teilstring der URL oder des Pfads> entscheiden.
165
+ „liegt in der Quelle … mehrfach": mit --pfad einen der genannten Pfade wählen. Liegt genau
166
+ eine Fassung im Skill-Ordner des Harness (claude-code .claude/skills, hermes .hermes/skills,
167
+ agents .agents/skills), nimmt hmgr sie von selbst und fragt nicht nach.
163
168
  „liegt bereits ein fremder Eintrag": --ersetzen setzen oder vorher hmgr adopt <name>.
164
169
  „gibt es keinen Zeiger": --version braucht ein Arbeitsverzeichnis mit composer.json und kein
165
170
  --local. Ohne Zeiger den Link ohne --version legen.
@@ -315,6 +320,11 @@ Liegt am Ziel schon derselbe Skill aus derselben Quelle als hmgr-Link, wird er a
315
320
  übernommen: aus Bestand wird ein Bundle, ohne erst Links von Hand zu entfernen. Alles andere unter
316
321
  demselben Namen ist ein Konflikt und wird genannt.
317
322
 
323
+ Ein Bundle in einem Arbeitsverzeichnis, das es nicht mehr gibt, legen update und sync nicht wieder
324
+ an: update übergeht es still, sync --ort meldet es als fehlgeschlagen. hmgr doctor bietet an, das
325
+ Arbeitsverzeichnis samt seinen Bundle-Installationen zu vergessen. bundle add legt ein fehlendes
326
+ Verzeichnis weiterhin an, bundle remove trägt kein Arbeitsverzeichnis ein.
327
+
318
328
  hmgr unlink <skill> auf ein einzelnes Mitglied nimmt es aus dem Bundle heraus: nur dieser Link
319
329
  verschwindet, das Bundle bleibt im Ziel und merkt sich den Skill als herausgenommen. sync und
320
330
  update legen ihn nicht wieder an, neue Mitglieder der Definition ergänzen sie weiter. Zurück kommt
@@ -617,7 +627,9 @@ Prüft, ob jedes gemerkte Arbeitsverzeichnis (hmgr dir add) noch auf der Platte
617
627
  umbenanntes oder woanders neu geklontes Projekt hinterlässt sonst für immer einen toten Pfad im
618
628
  Zustand, den nur hmgr dir forget von Hand entfernt. Bei einem Fund fragt doctor wie hmgr update
619
629
  einmal für alle Funde, ob sie vergessen werden sollen; ohne Bestätigung bleiben sie gemeldet, aber
620
- gemerkt. Anders als update holt doctor nichts aus dem Netz.
630
+ gemerkt. Geprüft werden auch Arbeitsverzeichnisse, an denen nur Bundles oder Bindungen hängen
631
+ (E119); vergessen nimmt dann auch deren Bundle-Installationen aus dem Zustand. Anders als update
632
+ holt doctor nichts aus dem Netz.
621
633
 
622
634
  Dazu prüft doctor Dateien, die eine Quelle mit SOPS verschlüsselt (.gitattributes mit
623
635
  filter=sops_dotenv, sops_yaml oder sops_generic) und die noch verschlüsselt im Checkout liegen,
package/dist/cli/main.js CHANGED
@@ -121,7 +121,7 @@ const SCHALTER = {
121
121
  source: { werte: ['name'], flaggen: [] },
122
122
  dir: { werte: ['composer'], flaggen: [] },
123
123
  link: {
124
- werte: ['harness', 'ort', 'composer', 'quelle', 'version'],
124
+ werte: ['harness', 'ort', 'composer', 'quelle', 'version', 'pfad'],
125
125
  flaggen: ['local', 'lokal', 'ersetzen'],
126
126
  },
127
127
  unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
@@ -483,7 +483,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
483
483
  'hmgr status zeigt die verlinkten Skills, hmgr source add <url|pfad> die Skills einer Quelle.');
484
484
  const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
485
485
  const lokal = flaggen.has('local') || flaggen.has('lokal');
486
- if (ziel.ort.art === 'arbeitsverzeichnis') {
486
+ // E119: unlink trägt ein Verzeichnis, das es nicht mehr gibt, nicht neu ein.
487
+ if (ziel.ort.art === 'arbeitsverzeichnis' &&
488
+ (kommando === 'link' || (await ports.zeiger.verzeichnisExistiert(ziel.ort.pfad)))) {
487
489
  // Verzeichnis merken und binden, damit status es wiederfindet und der Zeiger bekannt ist (E21).
488
490
  const composer = werte.get('composer');
489
491
  const verz = composer === undefined
@@ -505,10 +507,12 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
505
507
  }
506
508
  const quelle = werte.get('quelle');
507
509
  const version = werte.get('version');
510
+ const pfad = werte.get('pfad');
508
511
  const link = await anwendungsfaelle.linkLegen({
509
512
  skill,
510
513
  ziel,
511
514
  ...(quelle === undefined ? {} : { quelle }),
515
+ ...(pfad === undefined ? {} : { pfad }),
512
516
  ...(version === undefined ? {} : { version }),
513
517
  ersetzen: flaggen.has('ersetzen'),
514
518
  lokal,
@@ -657,7 +661,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
657
661
  if (name === undefined && unterkommando !== 'sync')
658
662
  throw new Error(`bundle ${unterkommando} braucht einen Bundle-Namen. hmgr bundle list zeigt die Bundles der bekannten Quellen.`);
659
663
  const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort'), ports.umgebung.plattform()) };
660
- if (ziel.ort.art === 'arbeitsverzeichnis') {
664
+ if (unterkommando !== 'remove' && ziel.ort.art === 'arbeitsverzeichnis') {
661
665
  // Wie bei link: Verzeichnis merken und binden, damit status es wiederfindet (E21).
662
666
  const composer = werte.get('composer');
663
667
  const verz = composer === undefined
@@ -1318,7 +1322,9 @@ function istModellFehler(fehler) {
1318
1322
  */
1319
1323
  function naechsterSchritt(fehler) {
1320
1324
  if (fehler instanceof SkillMehrdeutig)
1321
- return `Quelle eingrenzen: hmgr link ${fehler.name} --quelle <teilstring einer der genannten Quellen>`;
1325
+ return fehler.nurEineQuelle
1326
+ ? `Fassung wählen: hmgr link ${fehler.name} --pfad <einer der genannten Pfade>`
1327
+ : `Quelle eingrenzen: hmgr link ${fehler.name} --quelle <teilstring einer der genannten Quellen>`;
1322
1328
  if (fehler instanceof SkillUnbekannt)
1323
1329
  return 'Erst die Quelle hinzufügen: hmgr source add <url|pfad>. hmgr source list zeigt die bekannten Quellen.';
1324
1330
  if (fehler instanceof NameBelegt)
package/dist/gui/main.js CHANGED
@@ -228,6 +228,7 @@ function registriereIpc() {
228
228
  ...(skill.eigenerKontext === undefined ? {} : { eigenerKontext: skill.eigenerKontext }),
229
229
  ...(skill.argumentHinweis === undefined ? {} : { argumentHinweis: skill.argumentHinweis }),
230
230
  ...(skill.geaendert === undefined ? {} : { geaendert: skill.geaendert }),
231
+ ...(skill.jeHarness === undefined ? {} : { jeHarness: skill.jeHarness }),
231
232
  quelleText: quelleAlsText(eintrag.quelle),
232
233
  quelleName: quelleName(eintrag.quelle),
233
234
  quelleSchluessel: quelleSchluessel(eintrag.quelle),
@@ -886,6 +887,8 @@ function fensterOeffnen() {
886
887
  var dskBundleBoxSichtbar = false;
887
888
  var dskBundleText = null;
888
889
  var dskBundleZuVorher = null, dskBundleKoerperText = null;
890
+ // E121: ein Name mit mehreren Fassungen in der Quelle steht einmal, mit Vermerk statt Pfad.
891
+ var dskMehrfachAnzahl = null, dskMehrfachText = null;
889
892
  // Aufklappbereich je Zeile (E92): zu vor dem Klick, danach sichtbar mit der ungekürzten
890
893
  // Beschreibung; die Checkbox bleibt unberührt, der Dialog offen.
891
894
  var dskKoerperVorher = null;
@@ -920,6 +923,10 @@ function fensterOeffnen() {
920
923
  dskAufklapp?.click();
921
924
  dskKoerper?.querySelector('p')?.click();
922
925
  dskAufklappFreiAngehakt = dskEingabe?.checked ?? null;
926
+ var dskMehrfach = Array.from(document.querySelectorAll('#dsk-liste .wahlzeile'))
927
+ .filter((z) => z.querySelector('b')?.textContent === 'zmehrfach');
928
+ dskMehrfachAnzahl = dskMehrfach.length;
929
+ dskMehrfachText = dskMehrfach[0]?.querySelector('.mono')?.textContent ?? null;
923
930
  dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
924
931
  dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
925
932
  ?.replace(/\\s+/g, ' ').trim() ?? null;
@@ -1283,6 +1290,8 @@ function fensterOeffnen() {
1283
1290
  katalogDetails,
1284
1291
  katalogMarkenWarnend,
1285
1292
  dskMarken,
1293
+ dskMehrfachAnzahl,
1294
+ dskMehrfachText,
1286
1295
  dskKoerperVorher,
1287
1296
  dskKoerperNachher,
1288
1297
  dskKoerperText,
@@ -813,7 +813,7 @@
813
813
  /** Skill zu einem Mitglied einer Definition aus dem Skill-Katalog (E114 L1): gleiche Quelle, gleicher
814
814
  Pfad. null, wenn die Quelle (noch) nicht eingerichtet oder nicht lesbar ist. */
815
815
  function bundleMitgliedSkill(m) {
816
- return SKILLS.filter(function (s) { return s.quelleSchluessel === m.quelleSchluessel && s.pfad === m.pfad; })[0] || null;
816
+ return SKILLS.filter(function (s) { return s.quelleSchluessel === m.quelleSchluessel && (s.pfad === m.pfad || (s.jeHarness || []).indexOf(m.pfad) > -1); })[0] || null;
817
817
  }
818
818
 
819
819
  /** Skill eines nicht verlinkten Mitglieds über seinen Namen, gesucht unter den Mitgliedern der
@@ -2274,7 +2274,8 @@
2274
2274
  // Skill-Ordner berührt. Nicht der Pull-Zeitpunkt — der steht als „Zuletzt geprüft" in der
2275
2275
  // Update-Ansicht und wäre für alle Skills einer Quelle derselbe.
2276
2276
  var geaendert = datumDeutsch(skill.geaendert);
2277
- var nachRef = ' · Ref ' + skill.ref + (geaendert ? ' · geändert ' + geaendert : '');
2277
+ var nachRef = ' · Ref ' + skill.ref + (geaendert ? ' · geändert ' + geaendert : '')
2278
+ + (skill.jeHarness ? ' · ' + JE_HARNESS_VERMERK : '');
2278
2279
  if (herkunftQuelle) {
2279
2280
  herkunft.appendChild(quelleLinkElement(herkunftQuelle));
2280
2281
  herkunft.appendChild(e('b', null, nachRef));
@@ -2341,7 +2342,8 @@
2341
2342
  var sichtbar = 0;
2342
2343
  document.querySelectorAll('#katalog .zeile').forEach(function (li) {
2343
2344
  var skill = SKILLS[+li.dataset.index];
2344
- var passtSuche = !suche || suchform(skill.name).indexOf(suche) > -1 || suchform(skill.pfad).indexOf(suche) > -1;
2345
+ var passtSuche = !suche || suchform(skill.name).indexOf(suche) > -1 || suchform(skill.pfad).indexOf(suche) > -1
2346
+ || (skill.jeHarness || []).some(function (p) { return suchform(p).indexOf(suche) > -1; });
2345
2347
  var passtFilter = quellenFilter === 'alle' || skill.quelleSchluessel === quellenFilter;
2346
2348
  var an = passtSuche && passtFilter;
2347
2349
  li.hidden = !an;
@@ -2581,9 +2583,13 @@
2581
2583
  /** Quelle · [Pfad ·] Ref für eine Skill-Karte im „Skills hinzufügen"-Dialog (F106): der Pfad
2582
2584
  unterscheidet Skills, die unter demselben Namen mehrfach im selben Repo liegen (z. B. je
2583
2585
  Harness ein eigener Ordner). Ein Wurzel-Skill hat pfad === '' und zeigt kein Pfad-Segment. */
2586
+ var JE_HARNESS_VERMERK = 'je Harness eigene Fassung';
2587
+
2584
2588
  function skillQuelleBeschriftung(skill) {
2585
2589
  var teile = [skill.quelleName];
2586
- if (skill.pfad) teile.push(skill.pfad);
2590
+ // Mehrfach in der Quelle, je Harness eine eigene Fassung (E121): der Kern löst beim Verlinken auf.
2591
+ if (skill.jeHarness) teile.push(JE_HARNESS_VERMERK);
2592
+ else if (skill.pfad) teile.push(skill.pfad);
2587
2593
  teile.push('Ref ' + skill.ref);
2588
2594
  return teile.join(' · ');
2589
2595
  }
@@ -861,10 +861,10 @@ export class Anwendungsfaelle {
861
861
  ergebnis.push({
862
862
  quelle,
863
863
  ref,
864
- skills: skills.map((skill) => {
864
+ skills: this.fassungenZusammenfassen(skills.map((skill) => {
865
865
  const geaendert = daten.get(skill.pfad);
866
866
  return geaendert === undefined ? skill : { ...skill, geaendert };
867
- }),
867
+ })),
868
868
  });
869
869
  }
870
870
  catch (fehler) {
@@ -873,6 +873,39 @@ export class Anwendungsfaelle {
873
873
  }
874
874
  return ergebnis;
875
875
  }
876
+ /**
877
+ * Katalog-Zusammenfassung (E121): Ein Name, der in einer Quelle mehrfach vorkommt und mindestens
878
+ * einmal unmittelbar in einem Harness-Skill-Ordner liegt, erscheint einmal, mit `jeHarness`. Als
879
+ * Vertreter dient die Fassung im ersten passenden Harness-Ordner (Reihenfolge der Harnesses),
880
+ * sonst die erste. Alle anderen Namen bleiben unverändert, ebenso die Reihenfolge.
881
+ */
882
+ fassungenZusammenfassen(skills) {
883
+ const ordner = this.p.harnesses.map((h) => `${h.quellOrdner()}/`);
884
+ const direkt = (s, o) => s.pfad.startsWith(o) && !s.pfad.slice(o.length).includes('/');
885
+ const gruppen = new Map();
886
+ for (const s of skills)
887
+ gruppen.set(s.name, [...(gruppen.get(s.name) ?? []), s]);
888
+ const ergebnis = [];
889
+ for (const s of skills) {
890
+ const gruppe = gruppen.get(s.name);
891
+ if (gruppe.length < 2 || !gruppe.some((g) => ordner.some((o) => direkt(g, o)))) {
892
+ ergebnis.push(s);
893
+ continue;
894
+ }
895
+ if (gruppe[0] !== s)
896
+ continue; // Vertreter steht an der Stelle der ersten Fassung
897
+ let vertreter = gruppe[0];
898
+ for (const o of ordner) {
899
+ const treffer = gruppe.find((g) => direkt(g, o));
900
+ if (treffer !== undefined) {
901
+ vertreter = treffer;
902
+ break;
903
+ }
904
+ }
905
+ ergebnis.push({ ...vertreter, jeHarness: gruppe.map((g) => g.pfad) });
906
+ }
907
+ return ergebnis;
908
+ }
876
909
  /** Alle Marketplaces der bekannten Quellen, für `hmgr plugin list`. */
877
910
  async marketplaces() {
878
911
  const zustand = await this.p.zustand.lesen();
@@ -1279,6 +1312,21 @@ export class Anwendungsfaelle {
1279
1312
  for (const installation of installationen) {
1280
1313
  const ziel = Anwendungsfaelle.installationZiel(installation);
1281
1314
  const kopf = { ziel, bundle: installation.bundle, quelle: installation.quelle };
1315
+ // E119: Ein Arbeitsverzeichnis, das es nicht mehr gibt, legt der Abgleich nicht neu an. Ohne
1316
+ // genanntes Ziel übergeht er es still (die Meldung übernimmt doctor), mit Ziel ist es ein Fehlschlag.
1317
+ if (ziel.ort.art === 'arbeitsverzeichnis' && !(await this.p.zeiger.verzeichnisExistiert(ziel.ort.pfad))) {
1318
+ if (args?.ziel === undefined)
1319
+ continue;
1320
+ ergebnis.push({
1321
+ ...kopf,
1322
+ ergebnis: 'fehlgeschlagen',
1323
+ gelegt: [],
1324
+ entfernt: [],
1325
+ neueQuellen: [],
1326
+ fehler: `Das Arbeitsverzeichnis ${ziel.ort.pfad} gibt es nicht mehr. hmgr doctor bietet an, es zu vergessen.`,
1327
+ });
1328
+ continue;
1329
+ }
1282
1330
  try {
1283
1331
  const harness = await this.harnessGeprueft(ziel.harness);
1284
1332
  // Die Definition liegt in der Quelle: ohne Nachziehen bliebe eine Änderung unsichtbar,
@@ -1383,6 +1431,7 @@ export class Anwendungsfaelle {
1383
1431
  const zustand = await this.p.zustand.lesen();
1384
1432
  zustand.arbeitsverzeichnisse = zustand.arbeitsverzeichnisse.filter((p) => p !== verzeichnis);
1385
1433
  zustand.bindungen = zustand.bindungen.filter((b) => !(b.ziel.ort.art === 'arbeitsverzeichnis' && b.ziel.ort.pfad === verzeichnis));
1434
+ zustand.bundleInstallationen = zustand.bundleInstallationen.filter((i) => !(i.ort.art === 'arbeitsverzeichnis' && i.ort.pfad === verzeichnis));
1386
1435
  await this.p.zustand.schreiben(zustand);
1387
1436
  }
1388
1437
  /**
@@ -1406,7 +1455,15 @@ export class Anwendungsfaelle {
1406
1455
  async doctorPruefen(rueckfragen) {
1407
1456
  const zustand = await this.p.zustand.lesen();
1408
1457
  const fehlend = [];
1409
- for (const pfad of zustand.arbeitsverzeichnisse)
1458
+ // E119: auch Pfade, an denen nur Bindungen oder Bundle-Installationen hängen.
1459
+ const pfade = new Set(zustand.arbeitsverzeichnisse);
1460
+ for (const b of zustand.bindungen)
1461
+ if (b.ziel.ort.art === 'arbeitsverzeichnis')
1462
+ pfade.add(b.ziel.ort.pfad);
1463
+ for (const i of zustand.bundleInstallationen)
1464
+ if (i.ort.art === 'arbeitsverzeichnis')
1465
+ pfade.add(i.ort.pfad);
1466
+ for (const pfad of pfade)
1410
1467
  if (!(await this.p.zeiger.verzeichnisExistiert(pfad)))
1411
1468
  fehlend.push(pfad);
1412
1469
  let vergessen = [];
@@ -1542,7 +1599,7 @@ export class Anwendungsfaelle {
1542
1599
  throw new Error(`Mehrere Quellen passen auf „${hinweis}", bitte eingrenzen:\n${zeilen(treffer)}`);
1543
1600
  }
1544
1601
  /** Skill über alle Quellen suchen. Mehrere Treffer sind nur mit Quellenhinweis erlaubt. */
1545
- async skillFinden(name, quelleHinweis) {
1602
+ async skillFinden(name, quelleHinweis, wahl = {}) {
1546
1603
  const zustand = await this.p.zustand.lesen();
1547
1604
  const funde = [];
1548
1605
  for (const quelle of zustand.quellen) {
@@ -1550,16 +1607,33 @@ export class Anwendungsfaelle {
1550
1607
  continue;
1551
1608
  const ref = await this.p.quellen.standardRef(quelle);
1552
1609
  const skills = await this.p.quellen.skills(quelle, ref);
1553
- for (const skill of skills)
1554
- if (skill.name === name)
1555
- funde.push({ quelle, ref, skill });
1610
+ for (const skill of Anwendungsfaelle.skillTreffer(skills, name, wahl))
1611
+ funde.push({ quelle, ref, skill });
1556
1612
  }
1557
1613
  if (funde.length === 0)
1558
1614
  throw new SkillUnbekannt(name);
1559
1615
  if (funde.length > 1)
1560
- throw new SkillMehrdeutig(name, funde.map((f) => f.quelle));
1616
+ throw new SkillMehrdeutig(name, funde.map((f) => ({ quelle: f.quelle, pfad: f.skill.pfad })));
1561
1617
  return funde[0];
1562
1618
  }
1619
+ /**
1620
+ * Die Skills einer Quelle zu einem Namen (E121). Mit `pfad` zählt nur der Skill mit genau diesem
1621
+ * Pfad. Sonst gewinnt bei mehreren Fassungen die eine, die unmittelbar im Skill-Ordner des
1622
+ * Harness liegt (`<quellOrdner>/<ordner>`, eine Ebene); gibt es keine solche oder mehrere,
1623
+ * bleiben alle, und der Aufrufer meldet die Mehrdeutigkeit.
1624
+ */
1625
+ static skillTreffer(skills, name, wahl) {
1626
+ const gleichnamig = skills.filter((s) => s.name === name);
1627
+ if (wahl.pfad !== undefined) {
1628
+ const pfad = wahl.pfad.replace(/^\.?\/+|\/+$/g, '');
1629
+ return gleichnamig.filter((s) => s.pfad === pfad);
1630
+ }
1631
+ if (gleichnamig.length < 2 || wahl.quellOrdner === undefined)
1632
+ return gleichnamig;
1633
+ const praefix = `${wahl.quellOrdner}/`;
1634
+ const direkt = gleichnamig.filter((s) => s.pfad.startsWith(praefix) && !s.pfad.slice(praefix.length).includes('/'));
1635
+ return direkt.length === 1 ? direkt : gleichnamig;
1636
+ }
1563
1637
  /** Checkout anlegen oder erweitern, Ziel vorbereiten, Link setzen. Ohne Zeiger-Arbeit. */
1564
1638
  async linkAnlegen(harness, ziel, fund, ersetzen) {
1565
1639
  if (!harness.orte().includes(ziel.ort.art))
@@ -1603,8 +1677,12 @@ export class Anwendungsfaelle {
1603
1677
  if (!harness.orte().includes(args.ziel.ort.art))
1604
1678
  throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
1605
1679
  const version = args.version;
1680
+ const wahl = {
1681
+ quellOrdner: harness.quellOrdner(),
1682
+ ...(args.pfad === undefined ? {} : { pfad: args.pfad }),
1683
+ };
1606
1684
  if (version === undefined) {
1607
- const fund = await this.skillFinden(args.skill, args.quelle);
1685
+ const fund = await this.skillFinden(args.skill, args.quelle, wahl);
1608
1686
  const ergebnis = await this.linkAusFund(harness, args.ziel, fund, args.ersetzen, args.lokal);
1609
1687
  return Anwendungsfaelle.hinweisAnhaengen(ergebnis, await this.bundleMitgliedZurueckholen(args.ziel, fund));
1610
1688
  }
@@ -1616,7 +1694,7 @@ export class Anwendungsfaelle {
1616
1694
  // Eine Versionseinschränkung schreibt immer geteilt (siehe EinschraenkungOhneZeiger oben), das
1617
1695
  // trifft die Sperre also ausnahmslos, solange sie steht (E57).
1618
1696
  await this.zeigerSchreibenPruefen(args.ziel, args.lokal);
1619
- const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
1697
+ const fund = await this.fundAusZeiger(args.skill, version, args.quelle, wahl);
1620
1698
  const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
1621
1699
  await this.zeigerErgaenzen(zeigerPfad, fund, version);
1622
1700
  const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
@@ -1933,9 +2011,9 @@ export class Anwendungsfaelle {
1933
2011
  * gilt der Text als genauer Ref-Name gegen alle Refs, damit Tags und Branches ohne
1934
2012
  * Versions-Schema erreichbar bleiben. Nie auf den Standard-Ref ausweichen (B8).
1935
2013
  */
1936
- async fundAusZeiger(name, einschraenkung, quelleHinweis) {
2014
+ async fundAusZeiger(name, einschraenkung, quelleHinweis, wahl = {}) {
1937
2015
  if (einschraenkung === null)
1938
- return await this.skillFinden(name, quelleHinweis);
2016
+ return await this.skillFinden(name, quelleHinweis, wahl);
1939
2017
  const zustand = await this.p.zustand.lesen();
1940
2018
  const quellen = zustand.quellen.filter((q) => quellePasst(q, quelleHinweis));
1941
2019
  for (const quelle of quellen) {
@@ -1944,7 +2022,7 @@ export class Anwendungsfaelle {
1944
2022
  const ref = passenderRef(tags, einschraenkung);
1945
2023
  if (ref === null)
1946
2024
  continue;
1947
- const skill = (await this.p.quellen.skills(quelle, ref)).find((s) => s.name === name);
2025
+ const skill = Anwendungsfaelle.skillTreffer(await this.p.quellen.skills(quelle, ref), name, wahl)[0];
1948
2026
  if (skill)
1949
2027
  return { quelle, ref, skill };
1950
2028
  }
@@ -1952,7 +2030,7 @@ export class Anwendungsfaelle {
1952
2030
  const refs = await this.p.quellen.refs(quelle).catch(() => []);
1953
2031
  if (!refs.includes(einschraenkung))
1954
2032
  continue;
1955
- const skill = (await this.p.quellen.skills(quelle, einschraenkung)).find((s) => s.name === name);
2033
+ const skill = Anwendungsfaelle.skillTreffer(await this.p.quellen.skills(quelle, einschraenkung), name, wahl)[0];
1956
2034
  if (skill)
1957
2035
  return { quelle, ref: einschraenkung, skill };
1958
2036
  }
@@ -2029,7 +2107,7 @@ export class Anwendungsfaelle {
2029
2107
  }
2030
2108
  for (const [name, einschraenkung] of zeiger.skills) {
2031
2109
  try {
2032
- const fund = await this.fundAusZeiger(name, einschraenkung);
2110
+ const fund = await this.fundAusZeiger(name, einschraenkung, undefined, { quellOrdner: harness.quellOrdner() });
2033
2111
  const gefunden = eintraege.find((e) => e.art === 'link' && e.link.name === name);
2034
2112
  // Ein eigener Link auf einen anderen Ref weicht ab und wird umgelegt (E33). Das trifft
2035
2113
  // die Einschränkung, die inzwischen einen höheren Tag findet.
@@ -338,10 +338,24 @@ export class LinkAnlegenFehlgeschlagen extends Error {
338
338
  export class SkillMehrdeutig extends Error {
339
339
  name;
340
340
  quellen;
341
- constructor(name, quellen) {
342
- super(`Skill „${name}" liegt in mehreren Quellen: ${quellen.map(quelleSchluessel).join(', ')}`);
341
+ funde;
342
+ /** Die Mehrdeutigkeit kommt aus einer einzigen Quelle: `--pfad` hilft, nicht `--quelle` (E121). */
343
+ nurEineQuelle;
344
+ constructor(name, funde) {
345
+ const quellen = [];
346
+ for (const f of funde)
347
+ if (!quellen.some((q) => quelleSchluessel(q) === quelleSchluessel(f.quelle)))
348
+ quellen.push(f.quelle);
349
+ const zeilen = funde
350
+ .map((f) => ` ${quelleSchluessel(f.quelle)}: ${f.pfad === '' ? '(Repo-Wurzel)' : f.pfad}`)
351
+ .join('\n');
352
+ super(quellen.length === 1
353
+ ? `Skill „${name}" liegt in der Quelle ${quelleSchluessel(quellen[0])} mehrfach:\n${zeilen}`
354
+ : `Skill „${name}" liegt in mehreren Quellen:\n${zeilen}`);
343
355
  this.name = name;
344
356
  this.quellen = quellen;
357
+ this.funde = funde;
358
+ this.nurEineQuelle = quellen.length === 1;
345
359
  }
346
360
  }
347
361
  export class SkillUnbekannt extends Error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",