hmgr 0.0.23 → 0.0.25

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/gui/main.js CHANGED
@@ -187,7 +187,9 @@ function registriereIpc() {
187
187
  // Ebenfalls nur für die Anzeige (E68): der Renderer leitet daraus den Pfadtrenner ab, mit dem er
188
188
  // eigene Anzeigepfade zusammensetzt („~/.claude.json" bzw. „~\.claude.json").
189
189
  ipcMain.handle('hmgr:plattform', async () => auffangen(async () => process.platform));
190
- ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand()).map(zielZustandSerialisieren)));
190
+ // Ohne Plugin-Abfrage (E93): der Renderer liest `plugins` aus dem Ziel-Zustand nirgends, und die
191
+ // Abfrage kostet einen claude-Prozess je Ziel.
192
+ ipcMain.handle('hmgr:istZustand', async () => auffangen(async () => (await anwendungsfaelle.istZustand({ plugins: false })).map(zielZustandSerialisieren)));
191
193
  ipcMain.handle('hmgr:marketplaces', async () => auffangen(async () => (await anwendungsfaelle.marketplaces()).map((m) => ({
192
194
  quelleText: quelleAlsText(m.quelle),
193
195
  name: m.name,
@@ -819,10 +821,40 @@ function fensterOeffnen() {
819
821
  // fällt, was ins Ziel kommt, und zeigt je Bundle Beschreibung und Herkunft.
820
822
  var dskBundleBoxSichtbar = false;
821
823
  var dskBundleText = null;
824
+ // Aufklappbereich je Zeile (E92): zu vor dem Klick, danach sichtbar mit der ungekürzten
825
+ // Beschreibung; die Checkbox bleibt unberührt, der Dialog offen.
826
+ var dskKoerperVorher = null;
827
+ var dskKoerperNachher = null;
828
+ var dskKoerperText = null;
829
+ var dskAufklappZeileAngehakt = null;
830
+ var dskAufklappZeileGesperrt = null;
831
+ var dskAufklappAriaNachher = null;
832
+ var dskDialogOffenNachAufklappen = null;
833
+ var dskAufklappFreiAngehakt = null;
822
834
  if (dskKnopf) {
823
835
  dskKnopf.click();
824
836
  dskMarken = document.querySelector('#dsk-liste .wahlzeile .marken')?.textContent
825
837
  ?.replace(/\\s+/g, ' ').trim() ?? null;
838
+ var dskZeile = document.querySelector('#dsk-liste .wahlzeile:has(.aufklappknopf)');
839
+ var dskAufklapp = dskZeile?.querySelector('.aufklappknopf');
840
+ var dskKoerper = dskZeile?.querySelector('.wahlkoerper');
841
+ dskKoerperVorher = dskKoerper?.hidden ?? null;
842
+ dskAufklapp?.click();
843
+ dskKoerperNachher = dskKoerper?.hidden ?? null;
844
+ dskAufklappAriaNachher = dskAufklapp?.getAttribute('aria-expanded') ?? null;
845
+ dskKoerperText = dskKoerper?.textContent?.replace(/\\s+/g, ' ').trim() ?? null;
846
+ dskKoerper?.querySelector('p')?.click();
847
+ dskAufklappZeileAngehakt = dskZeile?.querySelector('input')?.checked ?? null;
848
+ dskAufklappZeileGesperrt = dskZeile?.querySelector('input')?.disabled ?? null;
849
+ dskDialogOffenNachAufklappen = document.getElementById('d-skills')?.open ?? null;
850
+ // Die Fixture-Zeile ist gesperrt (Link liegt schon), das beweist nichts über das Label:
851
+ // darum dieselbe Zeile entsperren, zu- und wieder aufklappen und in den Text klicken.
852
+ var dskEingabe = dskZeile?.querySelector('input');
853
+ if (dskEingabe) dskEingabe.disabled = false;
854
+ dskAufklapp?.click();
855
+ dskAufklapp?.click();
856
+ dskKoerper?.querySelector('p')?.click();
857
+ dskAufklappFreiAngehakt = dskEingabe?.checked ?? null;
826
858
  dskBundleBoxSichtbar = document.getElementById('dsk-bundlebox')?.hidden === false;
827
859
  dskBundleText = document.querySelector('#dsk-bundles .wahlzeile')?.textContent
828
860
  ?.replace(/\\s+/g, ' ').trim() ?? null;
@@ -1126,6 +1158,14 @@ function fensterOeffnen() {
1126
1158
  katalogDetails,
1127
1159
  katalogMarkenWarnend,
1128
1160
  dskMarken,
1161
+ dskKoerperVorher,
1162
+ dskKoerperNachher,
1163
+ dskKoerperText,
1164
+ dskAufklappZeileAngehakt,
1165
+ dskAufklappZeileGesperrt,
1166
+ dskAufklappAriaNachher,
1167
+ dskDialogOffenNachAufklappen,
1168
+ dskAufklappFreiAngehakt,
1129
1169
  dskBundleBoxSichtbar,
1130
1170
  dskBundleText,
1131
1171
  dzZieleText,
@@ -2300,6 +2300,53 @@
2300
2300
  return teile.join(' · ');
2301
2301
  }
2302
2302
 
2303
+ /** Aufklappbereich einer Wahlzeile im Skills-hinzufügen-Dialog (E92): ungekürzte description und
2304
+ die Frontmatter-Angaben aus E55. Der Knopf hängt am Label (absolut oben rechts, `txt` bekommt
2305
+ Platz dafür), der Körper als drittes Kind des Labels, damit er als Grid-Zeile über die volle
2306
+ Breite läuft. Der Zustand ist der Zustand von aria-expanded: er lebt nur, solange die Zeile
2307
+ lebt (je Dialog-Öffnung neu, immer zugeklappt), nicht in SKILL_OFFEN. Das Label schaltet
2308
+ sonst die Checkbox um, deshalb preventDefault auf Knopf und Körper. Ohne Beschreibung und
2309
+ ohne Details entsteht nichts. */
2310
+ function skillAufklappbereich(skill, label, txt, index) {
2311
+ var details = skillDetails(skill);
2312
+ if (!skill.beschreibung && details.length === 0) return;
2313
+ var koerperId = 'dsk-koerper-' + index;
2314
+ var knopf = e('button', 'aufklappknopf');
2315
+ knopf.type = 'button';
2316
+ knopf.setAttribute('aria-expanded', 'false');
2317
+ knopf.setAttribute('aria-controls', koerperId);
2318
+ knopf.setAttribute('aria-label', 'Beschreibung und Details zu ' + skill.name + ' ein- oder ausklappen');
2319
+ var pfeil = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
2320
+ pfeil.setAttribute('viewBox', '0 0 16 16');
2321
+ pfeil.setAttribute('class', 'akkordeonpfeil dunkel');
2322
+ pfeil.setAttribute('aria-hidden', 'true');
2323
+ pfeil.innerHTML = '<path d="M4 6.5 8 10.5 12 6.5"/>';
2324
+ knopf.appendChild(pfeil);
2325
+
2326
+ var koerper = e('div', 'wahlkoerper');
2327
+ koerper.id = koerperId;
2328
+ koerper.hidden = true;
2329
+ if (skill.beschreibung) koerper.appendChild(e('p', 'skilldetail skillbeschreibung', skill.beschreibung));
2330
+ details.forEach(function (d) {
2331
+ var zeile = e('p', 'skilldetail');
2332
+ zeile.appendChild(e('b', null, d.label + ': '));
2333
+ zeile.appendChild(document.createTextNode(d.text));
2334
+ koerper.appendChild(zeile);
2335
+ });
2336
+
2337
+ knopf.addEventListener('click', function (ev) {
2338
+ ev.preventDefault();
2339
+ var war = knopf.getAttribute('aria-expanded') === 'true';
2340
+ knopf.setAttribute('aria-expanded', war ? 'false' : 'true');
2341
+ koerper.hidden = war;
2342
+ });
2343
+ koerper.addEventListener('click', function (ev) { ev.preventDefault(); });
2344
+
2345
+ txt.classList.add('mitknopf');
2346
+ label.appendChild(knopf);
2347
+ label.appendChild(koerper);
2348
+ }
2349
+
2303
2350
  function oeffneSkillsHinzufuegen(z) {
2304
2351
  skillsKontext = { modus: 'einzeln', z: z };
2305
2352
  $('dsk-ziel').textContent = zielTitel(z);
@@ -2329,6 +2376,7 @@
2329
2376
  txt.appendChild(quelleSpanEl);
2330
2377
  if (schon) txt.appendChild(e('span', 'notiz', 'Hängt hier schon.'));
2331
2378
  label.appendChild(txt);
2379
+ skillAufklappbereich(skill, label, txt, i);
2332
2380
  box.appendChild(label);
2333
2381
  });
2334
2382
  fuelleBundleSektion(function (bundle) { return bundleHaengtIn([z], bundle); }, 1);
@@ -2374,6 +2422,7 @@
2374
2422
  txt.appendChild(e('span', 'notiz', 'Hängt schon in ' + (teile.length - fehltIn.length) + ' von ' + teile.length + ' Harnesses.'));
2375
2423
  }
2376
2424
  label.appendChild(txt);
2425
+ skillAufklappbereich(skill, label, txt, i);
2377
2426
  box.appendChild(label);
2378
2427
  });
2379
2428
  fuelleBundleSektion(function (bundle) { return bundleHaengtIn(teile, bundle); }, teile.length);
@@ -539,6 +539,15 @@ dialog::backdrop{background:rgba(9,45,37,.62)}
539
539
  .wahlzeile b{font-family:var(--schrift);font-weight:700;font-size:17px;letter-spacing:-.012em}
540
540
  .wahlzeile .mono{display:block;color:var(--tinte-weich);margin-top:3px}
541
541
  .wahlzeile .notiz{display:block;margin-top:8px;font-size:16px;color:var(--messing-satt);background:var(--messing-blass);border-left:3px solid var(--messing);padding:7px 10px;border-radius:0 2px 2px 0}
542
+ /* Aufklappbereich einer Wahlzeile im Skills-Dialog (E92). Der Knopf sitzt absolut oben rechts im Label
543
+ (die Zeile ist position:relative), die Textspalte lässt ihm Platz; der Körper ist ein eigenes
544
+ Grid-Kind über beide Spalten. Wie bei .zeile .skillkoerper braucht hidden eine eigene Regel. */
545
+ .wahlzeile .aufklappknopf{position:absolute;top:10px;right:10px}
546
+ .wahlzeile .mitknopf{padding-right:40px}
547
+ .wahlzeile .wahlkoerper{grid-column:1/-1;margin-top:2px;padding-top:12px;border-top:1px solid var(--strich-zart);cursor:text}
548
+ .wahlzeile .wahlkoerper[hidden]{display:none}
549
+ .wahlzeile .wahlkoerper b{font-size:inherit;font-weight:600;letter-spacing:normal}
550
+ .skillbeschreibung{white-space:pre-wrap}
542
551
  .nummer{
543
552
  font-family:var(--mono);font-size:16px;font-weight:500;width:28px;height:28px;flex:none;display:grid;
544
553
  place-items:center;border:1.5px solid var(--tinte);border-radius:50%;margin-top:1px;
@@ -1,8 +1,11 @@
1
1
  // Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
2
2
  import path from 'node:path';
3
3
  import { AbhaengigkeitFehlt, BundleMehrdeutig, BundleMitgliederFehlen, BundleNamenskonflikte, BundleNichtInstalliert, BundleUnbekannt, EinschraenkungOhneZeiger, FremderEintragUnbekannt, HarnessInaktiv, HarnessNichtVorhanden, HarnessTypUnbekannt, HeimAdresseUngueltig, HeimBereitsVorhanden, HeimFaehigkeitFehlt, HeimNurUnterWindows, HeimUnbekannt, KeinPassenderRef, McpEbeneNichtUnterstuetzt, McpFaehigkeitFehlt, McpOrtUnterGitWurzel, McpUebernahmeNichtMoeglich, McpUnbekannt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, bundleSchluessel, heimAdresseAlsText, heimAdresseZerlegen, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleMitNamen, quelleName, quelleNameAbleiten, quelleNameFrei, quelleSchluessel, zielSchluessel, } from "./modell.js";
4
+ import { mitBegrenzung } from "./begrenzung.js";
4
5
  import { passenderRef } from "./version.js";
5
6
  import { kannIstRegistriert, kannMcp, kannPlugins, kannProfile, kannSkillOrdner, kannZurueckbauen, kannZusaetzlicheHeime, } from "../ports/harness.js";
7
+ /** So viele `plugin list`-Prozesse laufen im Ist-Zustand höchstens gleichzeitig (E93). */
8
+ const PLUGIN_ABFRAGEN_GLEICHZEITIG = 6;
6
9
  /**
7
10
  * Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
8
11
  * Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
@@ -2314,8 +2317,13 @@ export class Anwendungsfaelle {
2314
2317
  gruende.set(name, zeiger?.skills.has(name) === true ? { art: 'zeiger' } : { art: 'manuell' });
2315
2318
  return gruende;
2316
2319
  }
2317
- /** Ist-Zustand: alle Ziele der vorhandenen Harnesses, aus dem Dateisystem gelesen. */
2318
- async istZustand() {
2320
+ /**
2321
+ * Ist-Zustand: alle Ziele der vorhandenen Harnesses, aus dem Dateisystem gelesen.
2322
+ * `plugins: false` lässt die Plugin-Abfrage aus (E93): sie ist ein Prozess je Ziel und für die
2323
+ * GUI, die das Feld nicht zeigt, nur Wartezeit. Dann fehlt `plugins` an allen Ziel-Zuständen.
2324
+ */
2325
+ async istZustand(optionen = {}) {
2326
+ const mitPlugins = optionen.plugins !== false;
2319
2327
  const zustand = await this.p.zustand.lesen();
2320
2328
  // Je Identität die Quelle, unter deren Schreibweise sie geführt wird (E36, E48), für die
2321
2329
  // Übernahme-Prognose. Bleibt über den ganzen Lauf gleich, die Prognose verändert nichts.
@@ -2340,6 +2348,8 @@ export class Anwendungsfaelle {
2340
2348
  return funde;
2341
2349
  };
2342
2350
  const ergebnis = [];
2351
+ // Zielliste einmal bilden: Vorabruf der Plugins und Hauptschleife gehen über dieselben Ziele.
2352
+ const plan = [];
2343
2353
  for (const harness of await this.vorhandeneHarnesses()) {
2344
2354
  const ziele = [...this.homeZiele(harness)];
2345
2355
  if (harness.orte().includes('profil') && kannProfile(harness))
@@ -2348,14 +2358,32 @@ export class Anwendungsfaelle {
2348
2358
  if (harness.orte().includes('arbeitsverzeichnis'))
2349
2359
  for (const pfad of zustand.arbeitsverzeichnisse)
2350
2360
  ziele.push(arbeitsverzeichnisZiel(harness.name, pfad));
2361
+ plan.push({ harness, ziele });
2362
+ }
2363
+ // Installierte Plugins je Ziel vorab, mit begrenzter Parallelität (E93): `claude plugin list`
2364
+ // ist ein Prozess je Ziel (cwd-gebunden, kein Sammelaufruf), nacheinander dauert das bei vielen
2365
+ // Arbeitsverzeichnissen Sekunden. Plugins nur am primären Homedir (E78): die claude-CLI dieses
2366
+ // Rechners kennt die Plugin-Konfiguration einer WSL-Distro nicht, ihre user-Liste gehörte zum
2367
+ // Windows-Home.
2368
+ const pluginListen = new Map();
2369
+ if (mitPlugins) {
2370
+ const abfragen = [];
2371
+ for (const { harness, ziele } of plan)
2372
+ if (kannPlugins(harness))
2373
+ for (const ziel of ziele)
2374
+ if (!(ziel.ort.art === 'home' && ziel.ort.heim !== undefined))
2375
+ abfragen.push({
2376
+ schluessel: zielSchluessel(ziel),
2377
+ abruf: () => harness.plugins.installierte(ziel).catch(() => []),
2378
+ });
2379
+ const listen = await mitBegrenzung(PLUGIN_ABFRAGEN_GLEICHZEITIG, abfragen.map((a) => a.abruf));
2380
+ abfragen.forEach((a, i) => pluginListen.set(a.schluessel, listen[i]));
2381
+ }
2382
+ for (const { harness, ziele } of plan) {
2351
2383
  for (const ziel of ziele) {
2352
2384
  const verzeichnis = harness.zielVerzeichnis(ziel.ort);
2353
2385
  const eintraege = await this.p.ziele.eintraege(ziel, verzeichnis).catch(() => []);
2354
- // Plugins nur am primären Homedir (E78): die claude-CLI dieses Rechners kennt die
2355
- // Plugin-Konfiguration einer WSL-Distro nicht, ihre user-Liste gehörte zum Windows-Home.
2356
- const plugins = kannPlugins(harness) && !(ziel.ort.art === 'home' && ziel.ort.heim !== undefined)
2357
- ? await harness.plugins.installierte(ziel).catch(() => [])
2358
- : undefined;
2386
+ const plugins = pluginListen.get(zielSchluessel(ziel));
2359
2387
  const zeiger = await this.zeigerFuerZiel(ziel);
2360
2388
  const istRegistriert = kannIstRegistriert(harness) ? await harness.istRegistriert(ziel.ort) : false;
2361
2389
  // Fähigkeit am Typwächter, nicht am Namen (E16), und nur an Orten, die sie kennt.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Führt Aufgaben mit höchstens `grenze` gleichzeitig laufenden aus (E93). Die Ergebnisse stehen
3
+ * in der Reihenfolge der Aufgaben, nicht der Fertigstellung. Kein Fehlerschutz: wirft eine
4
+ * Aufgabe, bricht der Aufruf mit diesem Fehler ab und startet keine weiteren Aufgaben. Aufrufer,
5
+ * die einzelne Ausfälle hinnehmen wollen, setzen das `.catch` in der Aufgabe selbst.
6
+ */
7
+ export async function mitBegrenzung(grenze, aufgaben) {
8
+ if (!Number.isInteger(grenze) || grenze < 1)
9
+ throw new RangeError(`Grenze muss eine ganze Zahl ab 1 sein, war ${grenze}`);
10
+ const ergebnisse = new Array(aufgaben.length);
11
+ let naechste = 0;
12
+ let abgebrochen = false;
13
+ const arbeiter = async () => {
14
+ while (!abgebrochen && naechste < aufgaben.length) {
15
+ const index = naechste++;
16
+ try {
17
+ ergebnisse[index] = await aufgaben[index]();
18
+ }
19
+ catch (fehler) {
20
+ abgebrochen = true;
21
+ throw fehler;
22
+ }
23
+ }
24
+ };
25
+ await Promise.all(Array.from({ length: Math.min(grenze, aufgaben.length) }, arbeiter));
26
+ return ergebnisse;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",