hmgr 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -26,34 +26,5 @@ npm run build
26
26
 
27
27
  ## Kommandos
28
28
 
29
- ```
30
- source add <url|pfad> Quelle hinzufügen (URL wird geklont, Pfad bleibt Arbeitskopie)
31
- source list Quellen zeigen
32
- dir add <pfad> Arbeitsverzeichnis merken und an seinen Zeiger binden
33
- dir forget <pfad> Arbeitsverzeichnis und Bindung vergessen, Links bleiben
34
- dir list Arbeitsverzeichnisse mit ihrer Bindung zeigen
35
- link <skill> Skill in ein Ziel verlinken
36
- unlink <skill> Link entfernen
37
- adopt [name] Fremde Links im Ziel durch eigene hmgr-Links ersetzen
38
- plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
39
- plugin remove <name> Plugin aus einem Ziel entfernen
40
- plugin list Plugins der Marketplaces aller Quellen
41
- sync [pfad] Ziel auf den Stand seines Zeigers bringen
42
- update Alle Checkouts aktualisieren
43
- status Ist-Zustand der Ziele
44
- ```
45
-
46
- Optionen:
47
-
48
- ```
49
- --harness <name> claude-code oder hermes, Standard claude-code
50
- --ort home|dir:<pfad>|profil:<name> Standard home
51
- --quelle <teilstring> Quelle bei mehrdeutigem Skill-Namen
52
- --version <spanne> Versionseinschränkung nach Composer-Regeln, nur mit Zeiger
53
- --composer <pfad> composer.json wählen, wenn mehrere in Frage kommen
54
- --local Link nur ins Ziel legen, nicht in den Zeiger
55
- --ersetzen fremden Eintrag am Ziel ersetzen
56
- --probelauf bei adopt nur melden, nichts schreiben
57
- ```
58
-
59
- `hmgr help` zeigt dieselbe Übersicht.
29
+ `hmgr --help` zeigt die Kommandos, `hmgr help <kommando>` Schalter, Beispiele
30
+ und Zusammenhänge.
@@ -0,0 +1,43 @@
1
+ // Das universelle Skill-Verzeichnis `.agents/skills`: ein Harness-Typ ohne Programm dahinter (E35).
2
+ // Home ist `~/.agents/skills`, das Arbeitsverzeichnis `<pfad>/.agents/skills`.
3
+ import { mkdir, stat } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { OrtNichtUnterstuetzt } from "../kern/modell.js";
6
+ import { gitignoreEintragen, istGitRepo } from "./gitignore.js";
7
+ const AGENTS_VERZEICHNIS = '.agents';
8
+ const SKILLS_ORDNER = 'skills';
9
+ const GITIGNORE_ZEILE = '.agents/';
10
+ export class Agents {
11
+ name = 'agents';
12
+ home;
13
+ /** `home` ist das HOME des Nutzers, das Verzeichnis liegt darunter als `.agents`. */
14
+ constructor(home) {
15
+ this.home = home;
16
+ }
17
+ /**
18
+ * Wie bei den anderen Harness-Typen zählt das angelegte Verzeichnis. Legt es niemand an, gibt es
19
+ * auf diesem Rechner keinen Konsumenten dafür und `status` überspringt das Ziel.
20
+ */
21
+ async vorhanden() {
22
+ return (await stat(path.join(this.home, AGENTS_VERZEICHNIS)).catch(() => undefined)) !== undefined;
23
+ }
24
+ orte() {
25
+ return ['home', 'arbeitsverzeichnis'];
26
+ }
27
+ zielVerzeichnis(ort) {
28
+ if (ort.art === 'home')
29
+ return path.join(this.home, AGENTS_VERZEICHNIS, SKILLS_ORDNER);
30
+ if (ort.art === 'arbeitsverzeichnis')
31
+ return path.join(ort.pfad, AGENTS_VERZEICHNIS, SKILLS_ORDNER);
32
+ throw new OrtNichtUnterstuetzt(this.name, ort.art);
33
+ }
34
+ async vorbereiten(ort) {
35
+ await mkdir(this.zielVerzeichnis(ort), { recursive: true });
36
+ // .agents/ darf nicht ins Repo (B7).
37
+ if (ort.art === 'arbeitsverzeichnis' && (await istGitRepo(ort.pfad)))
38
+ await gitignoreEintragen(ort.pfad, GITIGNORE_ZEILE);
39
+ }
40
+ folgtLinks() {
41
+ return true;
42
+ }
43
+ }
@@ -120,8 +120,9 @@ class ClaudePlugins {
120
120
  const { scope, cwd } = this.scope(ziel);
121
121
  await this.rufen(['plugin', 'uninstall', pluginSchluessel({ name: plugin, marketplace }), '--scope', scope], cwd);
122
122
  }
123
+ /** Ohne Namen aktualisiert die CLI alle Marketplaces. Je Plugin verlangt sie einen Namen, das übernimmt hmgr nicht (E14). */
123
124
  async aktualisieren() {
124
- await this.rufen(['plugin', 'update']);
125
+ await this.rufen(['plugin', 'marketplace', 'update']);
125
126
  }
126
127
  /**
127
128
  * Die Liste gilt nur für das cwd des Aufrufs und enthält dort alle Scopes. Zum Ort gehören im
@@ -1,6 +1,6 @@
1
1
  // Hermes Agent: Grundverhalten plus Profile entdecken und Verzeichnis in der config.yaml registrieren.
2
2
  // Skills landen nie in <hermes-home>/skills, sondern in einem Ordner aus skills.external_dirs (E13).
3
- import { readFileSync } from 'node:fs';
3
+ import { readFileSync, realpathSync } from 'node:fs';
4
4
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { Document, isSeq, parseDocument } from 'yaml';
@@ -29,6 +29,23 @@ function externalDirs(daten) {
29
29
  return [];
30
30
  return dirs.filter((d) => typeof d === 'string');
31
31
  }
32
+ /**
33
+ * Liest skills.trusted_project_dirs aus einer geparsten config.yaml. Hermes nimmt dort auch einen
34
+ * einzelnen String, der zählt wie eine Liste mit einem Eintrag.
35
+ */
36
+ function trustedProjectDirs(daten) {
37
+ if (typeof daten !== 'object' || daten === null)
38
+ return [];
39
+ const skills = daten['skills'];
40
+ if (typeof skills !== 'object' || skills === null)
41
+ return [];
42
+ const dirs = skills['trusted_project_dirs'];
43
+ if (typeof dirs === 'string')
44
+ return [dirs];
45
+ if (!Array.isArray(dirs))
46
+ return [];
47
+ return dirs.filter((d) => typeof d === 'string');
48
+ }
32
49
  export class Hermes {
33
50
  name = 'hermes';
34
51
  home;
@@ -125,6 +142,69 @@ export class Hermes {
125
142
  folgtLinks() {
126
143
  return true;
127
144
  }
145
+ /** Vergleich wie bei Hermes über den aufgelösten Pfad, Symlinks eingeschlossen. */
146
+ echterPfad(pfad) {
147
+ try {
148
+ return realpathSync(path.resolve(pfad));
149
+ }
150
+ catch {
151
+ return path.resolve(pfad);
152
+ }
153
+ }
154
+ /** Steht die Wurzel schon in `skills.trusted_project_dirs` der übergebenen config.yaml. */
155
+ vertraut(daten, hermesHome, wurzel) {
156
+ for (const eintrag of trustedProjectDirs(daten)) {
157
+ const pfad = this.aufloesen(eintrag, hermesHome);
158
+ if (pfad !== undefined && this.echterPfad(pfad) === wurzel)
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+ /**
164
+ * Fähigkeit Hinweis nach dem Link (E34): Hermes lädt `<wurzel>/.hermes/skills` nur, wenn die
165
+ * Wurzel in `skills.trusted_project_dirs` der config.yaml steht. hmgr trägt sie nicht von sich
166
+ * aus ein, sondern bietet es als Abhilfe an, entscheiden soll der Nutzer. Maßgeblich ist das
167
+ * Hermes-Home, ein Profil kennt der Ort Arbeitsverzeichnis nicht. Relative Einträge löst Hermes
168
+ * gegen das cwd auf, hmgr wie überall gegen das Hermes-Home, in der Praxis stehen dort absolute
169
+ * Pfade.
170
+ */
171
+ async hinweisNachLink(ort) {
172
+ if (ort.art !== 'arbeitsverzeichnis')
173
+ return undefined;
174
+ const hermesHome = path.join(this.home, HERMES_VERZEICHNIS);
175
+ const wurzel = ort.pfad;
176
+ if (this.vertraut(this.gelesen(hermesHome), hermesHome, this.echterPfad(wurzel)))
177
+ return undefined;
178
+ return {
179
+ hinweis: `Hermes lädt Projekt-Skills in ${wurzel} erst nach \`hermes skills trust ${wurzel}\`.`,
180
+ abhilfe: {
181
+ frage: `Hermes lädt Projekt-Skills in ${wurzel} erst nach Vertrauen. Jetzt als vertrauenswürdig eintragen? [j/N]`,
182
+ erledigt: `${wurzel} als vertrauenswürdig in Hermes eingetragen.`,
183
+ ausfuehren: async () => await this.vertrauen(wurzel),
184
+ },
185
+ };
186
+ }
187
+ /**
188
+ * Trägt die Wurzel in `skills.trusted_project_dirs` der config.yaml des Hermes-Home ein, falls
189
+ * sie fehlt. Geschrieben wird der aufgelöste absolute Pfad, so vergleicht Hermes selbst
190
+ * (HARNESS-FAKTEN). Kommentare bleiben erhalten, ein einzelner String wird zur Liste.
191
+ */
192
+ async vertrauen(wurzel) {
193
+ const hermesHome = path.join(this.home, HERMES_VERZEICHNIS);
194
+ const pfad = path.join(hermesHome, CONFIG);
195
+ const text = await readFile(pfad, 'utf8').catch(() => undefined);
196
+ const doc = text === undefined ? new Document({ skills: { trusted_project_dirs: [] } }) : parseDocument(text);
197
+ const eintrag = this.echterPfad(wurzel);
198
+ if (this.vertraut(doc.toJS(), hermesHome, eintrag))
199
+ return;
200
+ const liste = doc.getIn(['skills', 'trusted_project_dirs']);
201
+ if (isSeq(liste))
202
+ liste.add(eintrag);
203
+ else
204
+ doc.setIn(['skills', 'trusted_project_dirs'], [...trustedProjectDirs(doc.toJS()), eintrag]);
205
+ await mkdir(hermesHome, { recursive: true });
206
+ await writeFile(pfad, doc.toString());
207
+ }
128
208
  /** Fähigkeit Profile entdecken: Verzeichnisse mit config.yaml, bei jedem Aufruf frisch gelesen (E19). */
129
209
  async profile() {
130
210
  const basis = path.join(this.home, HERMES_VERZEICHNIS, PROFILE_VERZEICHNIS);
@@ -0,0 +1,24 @@
1
+ // Rückfrage auf dem Terminal. Bleibt dünn: eine Weiche, eine gelesene Zeile, kein eigenes Urteil.
2
+ import { createInterface } from 'node:readline/promises';
3
+ /** Antworten, die als Ja zählen. Alles andere, auch die leere Eingabe, ist Nein. */
4
+ const JA = new Set(['j', 'ja']);
5
+ export class TerminalRueckfrage {
6
+ /**
7
+ * Ohne Terminal auf beiden Seiten gibt es niemanden, der antworten könnte: hmgr läuft dann in
8
+ * einem Composer-Script, einer Pipe oder auf einem Deployment-Rechner (E20). Eine Frage ginge
9
+ * dort ins Leere und das Lesen von stdin bliebe hängen oder liefe sofort auf das Dateiende. Also
10
+ * gilt Nein, ohne Ausgabe: der Aufrufer bekommt seinen Hinweis und niemand wartet.
11
+ */
12
+ async bestaetigen(frage) {
13
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true)
14
+ return false;
15
+ const leser = createInterface({ input: process.stdin, output: process.stdout });
16
+ try {
17
+ const antwort = await leser.question(`${frage} `);
18
+ return JA.has(antwort.trim().toLowerCase());
19
+ }
20
+ finally {
21
+ leser.close();
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,254 @@
1
+ // Hilfetexte der CLI. Nur Zeichenketten, keine Ausgabe: die macht main.ts (Terminal-I/O).
2
+ /** Ebene 1: Orientierung, je Kommando ein Einzeiler, Verweis auf die Kommando-Hilfe. */
3
+ export const HILFE = `hmgr verlinkt Skills aus Git-Quellen in die Verzeichnisse, aus denen ein Harness sie lädt:
4
+ claude-code, hermes oder agents. Welche Skills und Plugins zu einem Arbeitsverzeichnis gehören,
5
+ steht in seinem Zeiger, dem extra-Abschnitt einer composer.json.
6
+
7
+ Arbeitszyklus: Skill in seiner Quelle ändern, committen und pushen, dann hmgr update. Ein frisch
8
+ geklontes Vorhaben holt seinen Stand mit hmgr sync.
9
+
10
+ hmgr <kommando> [schalter]
11
+
12
+ source add <url|pfad> Quelle hinzufügen (URL wird geklont, Pfad bleibt Arbeitskopie)
13
+ source list Quellen zeigen
14
+ dir add <pfad> Arbeitsverzeichnis merken und an seinen Zeiger binden
15
+ dir forget <pfad> Arbeitsverzeichnis und Bindung vergessen, Links bleiben
16
+ dir list Arbeitsverzeichnisse mit ihrer Bindung zeigen
17
+ link <skill> Skill in ein Ziel verlinken
18
+ unlink <skill> Link entfernen
19
+ adopt [name] Fremde Links im Ziel durch eigene hmgr-Links ersetzen
20
+ plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
21
+ plugin remove <name> Plugin aus einem Ziel entfernen
22
+ plugin list Plugins der Marketplaces aller Quellen
23
+ sync [pfad] Ziel auf den Stand seines Zeigers bringen
24
+ update Alle Checkouts aktualisieren
25
+ status Ist-Zustand der Ziele
26
+
27
+ hmgr help <kommando> zeigt Schalter, Beispiele und Zusammenhänge.`;
28
+ const SOURCE = `hmgr source add <url|pfad>
29
+ hmgr source list
30
+
31
+ Eine Quelle ist ein Git-Repo mit Skills. source add prüft, ob sie erreichbar ist, liest ihre Skills
32
+ und ihren Marketplace und merkt sie unter ~/.hmgr. Erst danach kennt hmgr die Skill-Namen, mit
33
+ denen link und plugin add arbeiten. Eine URL wird geklont, ein Pfad bleibt die Arbeitskopie, die
34
+ dort schon liegt, und wird von hmgr nicht verändert.
35
+
36
+ Schalter: keine.
37
+
38
+ Beispiele:
39
+ hmgr source add https://github.com/tim/skills.git
40
+ hmgr source add ~/.skills/tim-skills
41
+ hmgr source list
42
+
43
+ Häufige Fälle:
44
+ „Skill … liegt in keiner Quelle": die Quelle fehlt, erst source add, dann link.
45
+ Neue Commits einer bekannten Quelle holt hmgr update, nicht source add.
46
+
47
+ Siehe auch: hmgr help link, hmgr help plugin, hmgr help update`;
48
+ const DIR = `hmgr dir add <pfad> [--composer <pfad>]
49
+ hmgr dir forget <pfad>
50
+ hmgr dir list
51
+
52
+ Ein Arbeitsverzeichnis ist ein Verzeichnis, in dem ein Harness gestartet wird. dir add merkt es und
53
+ bindet es an seinen Zeiger, also an die composer.json, in deren extra-Abschnitt Quellen, Skills und
54
+ Plugins des Vorhabens stehen. Ohne Zeiger bleibt das Verzeichnis gemerkt und jeder Link dort lokal
55
+ (E26). dir forget löst Bindung und Eintrag, die gelegten Links bleiben liegen.
56
+
57
+ Schalter:
58
+ --composer <pfad> composer.json wählen, wenn unter dem Verzeichnis mehrere in Frage kommen
59
+
60
+ Beispiele:
61
+ hmgr dir add ~/Code/site
62
+ hmgr dir add ~/Code/site --composer ~/Code/site/app/composer.json
63
+ hmgr dir list
64
+
65
+ Häufige Fälle:
66
+ „Mehrere composer.json unter …": eine mit --composer nennen, hmgr rät nicht.
67
+ link --ort dir:<pfad> merkt das Verzeichnis von selbst. dir add braucht es vorher nur, wenn die
68
+ composer.json mehrdeutig ist.
69
+
70
+ Siehe auch: hmgr help link, hmgr help sync`;
71
+ const LINK = `hmgr link <skill> [schalter]
72
+
73
+ Legt im Ziel einen Symlink auf den Skill an, der aus einer bekannten Quelle stammt, und trägt ihn
74
+ in den Zeiger des Ziels ein, damit hmgr sync ihn anderswo reproduzieren kann. Einen Zeiger gibt es
75
+ nur für ein Arbeitsverzeichnis mit composer.json, an Home und Profil bleibt jeder Link lokal.
76
+
77
+ Schalter:
78
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
79
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
80
+ --quelle <teil> Teilstring der Quelle, wenn der Skill-Name in mehreren Quellen vorkommt
81
+ --version <spanne> Versionseinschränkung nach Composer-Regeln (etwa ^1.2) oder ein Tag- oder
82
+ Branchname. Nur mit Zeiger, also dir:<pfad> ohne --local (E33)
83
+ --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
84
+ --local Link nur ins Ziel legen, nicht in den Zeiger
85
+ --ersetzen einen fremden Eintrag gleichen Namens am Ziel ersetzen
86
+
87
+ Beispiele:
88
+ hmgr link netzwerk
89
+ hmgr link netzwerk --harness hermes --ort profil:researcher
90
+ hmgr link tq-schema --ort dir:. --version ^2.0
91
+
92
+ Häufige Fälle:
93
+ „liegt in mehreren Quellen": mit --quelle <teilstring der URL oder des Pfads> entscheiden.
94
+ „liegt bereits ein fremder Eintrag": --ersetzen setzen oder vorher hmgr adopt <name>.
95
+ „gibt es keinen Zeiger": --version braucht ein Arbeitsverzeichnis mit composer.json und kein
96
+ --local. Ohne Zeiger den Link ohne --version legen.
97
+
98
+ Siehe auch: hmgr help unlink, hmgr help sync, hmgr help adopt`;
99
+ const UNLINK = `hmgr unlink <skill> [schalter]
100
+
101
+ Entfernt den Link aus dem Ziel und, wenn das Ziel an einen Zeiger gebunden ist, den Skill aus dem
102
+ Zeiger. Der Checkout unter ~/.hmgr bleibt liegen, Links anderer Ziele bleiben unberührt.
103
+
104
+ Schalter:
105
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
106
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
107
+ --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
108
+ --local nur den Link entfernen, den Eintrag im Zeiger stehen lassen
109
+
110
+ Beispiele:
111
+ hmgr unlink netzwerk
112
+ hmgr unlink netzwerk --ort dir:. --local
113
+
114
+ Häufige Fälle:
115
+ Bleibt der Skill im Zeiger stehen, legt hmgr sync ihn wieder an. Ohne --local räumt unlink
116
+ beides ab.
117
+
118
+ Siehe auch: hmgr help link, hmgr help sync`;
119
+ const ADOPT = `hmgr adopt [name] [schalter]
120
+
121
+ Ersetzt fremde Symlinks im Ziel durch eigene hmgr-Links: hmgr liest, aus welcher Git-Arbeitskopie
122
+ ein Link stammt, nimmt deren remote.origin.url als Quelle, legt einen Checkout am selben Branch an
123
+ und verlinkt den Skill daraus. Ohne Namen laufen alle fremden Einträge des Ziels durch, mit Namen
124
+ nur dieser eine. Was sich nicht auflösen lässt, bleibt liegen und wird mit Grund gemeldet.
125
+
126
+ Schalter:
127
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
128
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
129
+ --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
130
+ --local Übernahme nicht in den Zeiger schreiben
131
+ --probelauf nur melden, nichts schreiben
132
+
133
+ Beispiele:
134
+ hmgr adopt --probelauf
135
+ hmgr adopt netzwerk --ort dir:.
136
+
137
+ Häufige Fälle:
138
+ „kein Symlink": der Eintrag ist ein echtes Verzeichnis, hmgr fasst ihn nicht an.
139
+ „liegt in keiner Git-Arbeitskopie" oder „ohne remote.origin.url": die Quelle ist nicht
140
+ bestimmbar. Mit hmgr source add <url> eine Quelle nennen und mit hmgr link --ersetzen verlinken.
141
+ „ohne Branch (detached HEAD)": in der Arbeitskopie einen Branch auschecken, dann erneut adopt.
142
+ „Pfad ist in der Quelle kein Skill": im Repo liegt an der Stelle keine SKILL.md.
143
+
144
+ Siehe auch: hmgr help link, hmgr help status`;
145
+ const PLUGIN = `hmgr plugin add <name> [schalter]
146
+ hmgr plugin remove <name> [schalter]
147
+ hmgr plugin list
148
+
149
+ Ein Plugin ist ein Bündel aus einem Marketplace, also aus einer Quelle mit
150
+ .claude-plugin/marketplace.json. Hat der Harness die Plugin-Fähigkeit (claude-code), lässt hmgr das
151
+ Plugin von dessen CLI installieren, sonst verlinkt es die flache Skill-Liste des Plugins samt
152
+ seinen Abhängigkeiten. In den Zeiger kommt das Plugin, nicht seine Einzelskills. plugin list zeigt
153
+ die Marketplaces aller bekannten Quellen mit ihren Plugins.
154
+
155
+ Schalter (add und remove):
156
+ --harness <name> claude-code, hermes oder agents, Standard claude-code
157
+ --ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
158
+ --composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
159
+ --local Plugin nicht in den Zeiger schreiben
160
+ --quelle <teil> nur bei add: Quelle eingrenzen, wenn der Name in mehreren Marketplaces steht
161
+
162
+ Beispiele:
163
+ hmgr plugin list
164
+ hmgr plugin add superpowers --ort dir:.
165
+ hmgr plugin remove superpowers
166
+
167
+ Häufige Fälle:
168
+ „Keine Quelle mit Marketplace": erst hmgr source add <url> der Quelle mit der marketplace.json.
169
+ „liegt in keinem Marketplace der bekannten Quellen": Namen mit hmgr plugin list prüfen.
170
+ „liegt in mehreren Marketplaces": mit --quelle <teilstring> entscheiden.
171
+ „braucht …, das im selben Marketplace nicht steht": die Abhängigkeit fehlt im Marketplace, das
172
+ kann nur die Quelle beheben.
173
+
174
+ Siehe auch: hmgr help source, hmgr help sync`;
175
+ const SYNC = `hmgr sync [pfad]
176
+
177
+ Bringt ein Arbeitsverzeichnis auf den Stand seines Zeigers: fehlende Quellen übernehmen, fehlende
178
+ Links legen, Links auf einen anderen Ref umlegen, Plugins herstellen. Ohne Pfad gilt das aktuelle
179
+ Verzeichnis. Das ist der Befehl nach einem frischen Clone eines Vorhabens und nach jeder Änderung
180
+ am Zeiger. Eigene Links, die im Zeiger fehlen, werden als abweichend gemeldet und nicht entfernt,
181
+ fremde Einträge bleiben unberührt.
182
+
183
+ Schalter: keine.
184
+
185
+ Beispiele:
186
+ hmgr sync
187
+ hmgr sync ~/Code/site
188
+
189
+ Häufige Fälle:
190
+ „Kein Zeiger unter …, nichts zu tun": im Verzeichnis liegt keine composer.json. Mit
191
+ hmgr dir add <pfad> --composer <pfad> eine nennen.
192
+ „steht nicht im Zeiger": den Link mit hmgr link <skill> --ort dir:<pfad> eintragen oder mit
193
+ hmgr unlink <skill> --ort dir:<pfad> --local entfernen.
194
+ „Kein Harness auf diesem Rechner": weder ~/.claude noch ~/.hermes noch ~/.agents ist angelegt.
195
+
196
+ Siehe auch: hmgr help dir, hmgr help link, hmgr help update`;
197
+ const UPDATE = `hmgr update
198
+
199
+ Holt für jeden Checkout unter ~/.hmgr den neuesten Stand seines Refs und zieht bei Harnesses mit
200
+ Plugin-Fähigkeit deren Plugins nach (E14). Die Links zeigen weiter auf dieselben Pfade und müssen
201
+ nicht erneuert werden. Ein Checkout mit lokaler Änderung wird übersprungen und gemeldet (E15),
202
+ damit nichts verloren geht. Lokale Quellen haben keinen Checkout und bleiben unberührt.
203
+
204
+ So wird eine Änderung an einem Skill überall wirksam: in der Quelle committen und pushen, dann
205
+ hmgr update.
206
+
207
+ Schalter: keine.
208
+
209
+ Beispiel:
210
+ hmgr update
211
+
212
+ Häufige Fälle:
213
+ „übersprungen … lokale Änderung (E15)": im Checkout liegt eine Änderung, die durch einen Link
214
+ hindurch gemacht wurde. Dort committen und pushen, dann erneut hmgr update.
215
+ „Keine Checkouts": es gibt nur lokale Quellen oder noch keinen Link.
216
+
217
+ Siehe auch: hmgr help sync, hmgr help status`;
218
+ const STATUS = `hmgr status
219
+
220
+ Liest den Ist-Zustand vom Dateisystem: je Ziel eines vorhandenen Harness die Einträge mit Quelle
221
+ und Ref, davor die Versionseinschränkung, falls der Zeiger eine nennt. Fremde Einträge und Links
222
+ ohne bekannte Quelle sind eigens gekennzeichnet, dazu kommen die Plugins des Ziels. Am Ende stehen
223
+ die Harnesses, die auf diesem Rechner nicht liegen.
224
+
225
+ Schalter: keine.
226
+
227
+ Beispiel:
228
+ hmgr status
229
+
230
+ Häufige Fälle:
231
+ „fremd": der Eintrag stammt nicht von hmgr. hmgr adopt <name> übernimmt ihn.
232
+ „Link ohne bekannte Quelle": der Link zeigt in einen hmgr-Checkout, dessen Quelle nicht mehr im
233
+ Zustand steht. Mit hmgr source add <url> die Quelle wieder eintragen.
234
+ „Nicht vorhanden": der Harness hat sein Verzeichnis auf diesem Rechner nicht angelegt, hmgr
235
+ überspringt seine Ziele.
236
+
237
+ Siehe auch: hmgr help adopt, hmgr help sync`;
238
+ /** Ebene 2, je Kommandofamilie ein Text. Unterkommandos teilen sich die Hilfe ihrer Familie. */
239
+ const THEMEN = new Map([
240
+ ['source', SOURCE],
241
+ ['dir', DIR],
242
+ ['link', LINK],
243
+ ['unlink', UNLINK],
244
+ ['adopt', ADOPT],
245
+ ['plugin', PLUGIN],
246
+ ['sync', SYNC],
247
+ ['update', UPDATE],
248
+ ['status', STATUS],
249
+ ['help', HILFE],
250
+ ]);
251
+ /** Hilfetext zu einem Kommando, undefined für ein unbekanntes Thema. */
252
+ export function hilfeText(thema) {
253
+ return THEMEN.get(thema);
254
+ }
package/dist/cli/main.js CHANGED
@@ -6,13 +6,16 @@ import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
8
8
  import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, pluginSchluessel, } from "../kern/modell.js";
9
+ import { Agents } from "../adapter/agents.js";
9
10
  import { ClaudeCode } from "../adapter/claude-code.js";
10
11
  import { Hermes } from "../adapter/hermes.js";
11
12
  import { ComposerZeiger } from "../adapter/composer.js";
12
13
  import { DateisystemZiele } from "../adapter/dateisystem.js";
13
14
  import { GitQuellen } from "../adapter/git.js";
14
15
  import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
16
+ import { TerminalRueckfrage } from "../adapter/terminal.js";
15
17
  import { ZustandJson } from "../adapter/zustand-json.js";
18
+ import { HILFE, hilfeText } from "./hilfe.js";
16
19
  export function umgebungLesen(env = process.env) {
17
20
  const home = env.HOME ?? homedir();
18
21
  return { home, hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr') };
@@ -24,17 +27,18 @@ export function bauen(umgebung, optionen = {}) {
24
27
  ? new ClaudeCode(umgebung.home)
25
28
  : new ClaudeCode(umgebung.home, optionen.claudeAusfuehrer);
26
29
  const ports = {
27
- harnesses: [claude, new Hermes(umgebung.home)],
30
+ harnesses: [claude, new Hermes(umgebung.home), new Agents(umgebung.home)],
28
31
  quellen: new GitQuellen(wurzel),
29
32
  ziele: new DateisystemZiele(wurzel, zustand),
30
33
  zeiger: new ComposerZeiger(),
31
34
  zustand,
35
+ rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
32
36
  };
33
37
  return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
34
38
  }
35
- /** Schalter ohne Wert. Alles andere braucht einen. */
36
- const FLAGGEN = new Set(['ersetzen', 'local', 'lokal', 'probelauf']);
37
- function schalterLesen(args) {
39
+ /** Schalter ohne Wert. Alles andere braucht einen. `help` steht hier, damit --help nie greift. */
40
+ const FLAGGEN = new Set(['ersetzen', 'local', 'lokal', 'probelauf', 'help']);
41
+ function schalterLesen(args, kommando) {
38
42
  const rest = [];
39
43
  const werte = new Map();
40
44
  const flaggen = new Set();
@@ -51,7 +55,8 @@ function schalterLesen(args) {
51
55
  }
52
56
  const wert = args[++i];
53
57
  if (wert === undefined)
54
- throw new Error(`Schalter --${name} braucht einen Wert`);
58
+ throw new Error(`Schalter --${name} braucht einen Wert, etwa --${name} <wert>. ` +
59
+ `${kommando === undefined ? 'hmgr --help' : `hmgr help ${kommando}`} zeigt die Schalter.`);
55
60
  werte.set(name, wert);
56
61
  }
57
62
  return { rest, werte, flaggen };
@@ -66,31 +71,6 @@ function ortLesen(text) {
66
71
  return { art: 'profil', name: roh.slice(7) };
67
72
  throw new Error(`Unbekannter Ort „${roh}", erwartet home, dir:<pfad> oder profil:<name>`);
68
73
  }
69
- const HILFE = `hmgr <kommando>
70
-
71
- source add <url|pfad> Quelle hinzufügen (URL wird geklont, Pfad bleibt Arbeitskopie)
72
- source list Quellen zeigen
73
- dir add <pfad> Arbeitsverzeichnis merken und an seinen Zeiger binden
74
- dir forget <pfad> Arbeitsverzeichnis und Bindung vergessen, Links bleiben
75
- dir list Arbeitsverzeichnisse mit ihrer Bindung zeigen
76
- link <skill> Skill in ein Ziel verlinken
77
- unlink <skill> Link entfernen
78
- adopt [name] Fremde Links im Ziel durch eigene hmgr-Links ersetzen
79
- plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
80
- plugin remove <name> Plugin aus einem Ziel entfernen
81
- plugin list Plugins der Marketplaces aller Quellen
82
- sync [pfad] Ziel auf den Stand seines Zeigers bringen
83
- update Alle Checkouts aktualisieren
84
- status Ist-Zustand der Ziele
85
-
86
- --harness <name> claude-code oder hermes, Standard claude-code
87
- --ort home|dir:<pfad>|profil:<name> Standard home
88
- --quelle <teilstring> Quelle bei mehrdeutigem Skill-Namen
89
- --version <spanne> Versionseinschränkung nach Composer-Regeln, nur mit Zeiger (E33)
90
- --composer <pfad> composer.json wählen, wenn mehrere in Frage kommen
91
- --local Link nur ins Ziel legen, nicht in den Zeiger
92
- --ersetzen fremden Eintrag am Ziel ersetzen
93
- --probelauf bei adopt nur melden, nichts schreiben`;
94
74
  /** Klartext für den Grund, aus dem ein fremder Eintrag liegen bleibt (B12). */
95
75
  const UEBERNAHME_GRUND = {
96
76
  'kein-link': 'kein Symlink',
@@ -102,16 +82,36 @@ const UEBERNAHME_GRUND = {
102
82
  export async function ausfuehren(argv, umgebung, optionen = {}) {
103
83
  const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
104
84
  const kommando = argv[0];
105
- if (kommando === undefined || kommando === 'help' || kommando === '--help') {
85
+ // Hilfe zuerst: --help ist überall eine Flagge, damit kein Kommando sie als Wert liest.
86
+ if (kommando === undefined) {
106
87
  console.log(HILFE);
107
- return kommando === undefined ? 1 : 0;
88
+ return 1;
89
+ }
90
+ if (kommando === 'help' || kommando === '--help' || kommando === '-h') {
91
+ const thema = argv[1];
92
+ if (thema === undefined || thema.startsWith('-')) {
93
+ console.log(HILFE);
94
+ return 0;
95
+ }
96
+ const text = hilfeText(thema);
97
+ if (text === undefined)
98
+ throw new Error(`Unbekanntes Hilfe-Thema „${thema}". hmgr --help zeigt alle Kommandos.`);
99
+ console.log(text);
100
+ return 0;
101
+ }
102
+ if (argv.slice(1).includes('--help')) {
103
+ const text = hilfeText(kommando);
104
+ if (text === undefined)
105
+ throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
106
+ console.log(text);
107
+ return 0;
108
108
  }
109
109
  if (kommando === 'source') {
110
110
  const unterkommando = argv[1];
111
111
  if (unterkommando === 'add') {
112
112
  const text = argv[2];
113
113
  if (text === undefined)
114
- throw new Error('source add braucht eine URL oder einen Pfad');
114
+ throw new Error('source add braucht eine URL oder einen Pfad, etwa hmgr source add https://example.com/skills.git');
115
115
  const quelle = quelleAusText(text);
116
116
  const { skills, marketplace } = await anwendungsfaelle.quelleHinzufuegen(quelle);
117
117
  console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}), ${skills.length} Skills:`);
@@ -134,15 +134,15 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
134
134
  console.log(`${quelle.art} ${quelleAlsText(quelle)}`);
135
135
  return 0;
136
136
  }
137
- throw new Error('source add <url|pfad> oder source list');
137
+ throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
138
138
  }
139
139
  if (kommando === 'dir') {
140
- const { rest, werte } = schalterLesen(argv.slice(1));
140
+ const { rest, werte } = schalterLesen(argv.slice(1), 'dir');
141
141
  const unterkommando = rest[0];
142
142
  if (unterkommando === 'add') {
143
143
  const pfad = rest[1];
144
144
  if (pfad === undefined)
145
- throw new Error('dir add braucht einen Pfad');
145
+ throw new Error('dir add braucht einen Pfad, etwa hmgr dir add .');
146
146
  const composer = werte.get('composer');
147
147
  const ergebnis = composer === undefined
148
148
  ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)
@@ -157,7 +157,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
157
157
  if (unterkommando === 'forget') {
158
158
  const pfad = rest[1];
159
159
  if (pfad === undefined)
160
- throw new Error('dir forget braucht einen Pfad');
160
+ throw new Error('dir forget braucht einen Pfad. hmgr dir list zeigt die gemerkten Arbeitsverzeichnisse.');
161
161
  await anwendungsfaelle.arbeitsverzeichnisVergessen(pfad);
162
162
  console.log(`Arbeitsverzeichnis ${path.resolve(pfad)} vergessen, die Links bleiben liegen.`);
163
163
  return 0;
@@ -172,13 +172,14 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
172
172
  }
173
173
  return 0;
174
174
  }
175
- throw new Error('dir add <pfad>, dir forget <pfad> oder dir list');
175
+ throw new Error(`${unterkommando === undefined ? 'dir braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für dir`}: dir add <pfad>, dir forget <pfad> oder dir list. hmgr help dir zeigt Beispiele.`);
176
176
  }
177
177
  if (kommando === 'link' || kommando === 'unlink') {
178
- const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
178
+ const { rest, werte, flaggen } = schalterLesen(argv.slice(1), kommando);
179
179
  const skill = rest[0];
180
180
  if (skill === undefined)
181
- throw new Error(`${kommando} braucht einen Skill-Namen`);
181
+ throw new Error(`${kommando} braucht einen Skill-Namen, etwa hmgr ${kommando} <skill>. ` +
182
+ 'hmgr status zeigt die verlinkten Skills, hmgr source add <url|pfad> die Skills einer Quelle.');
182
183
  const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
183
184
  const lokal = flaggen.has('local') || flaggen.has('lokal');
184
185
  if (ziel.ort.art === 'arbeitsverzeichnis') {
@@ -211,7 +212,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
211
212
  return 0;
212
213
  }
213
214
  if (kommando === 'adopt') {
214
- const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
215
+ const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'adopt');
215
216
  const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
216
217
  const probelauf = flaggen.has('probelauf');
217
218
  const name = rest[0];
@@ -250,7 +251,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
250
251
  return 0;
251
252
  }
252
253
  if (kommando === 'plugin') {
253
- const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
254
+ const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'plugin');
254
255
  const unterkommando = rest[0];
255
256
  if (unterkommando === 'list') {
256
257
  const marketplaces = await anwendungsfaelle.marketplaces();
@@ -269,10 +270,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
269
270
  return 0;
270
271
  }
271
272
  if (unterkommando !== 'add' && unterkommando !== 'remove')
272
- throw new Error('plugin add <name>, plugin remove <name> oder plugin list');
273
+ throw new Error(`${unterkommando === undefined ? 'plugin braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für plugin`}: plugin add <name>, plugin remove <name> oder plugin list. hmgr help plugin zeigt Beispiele.`);
273
274
  const name = rest[1];
274
275
  if (name === undefined)
275
- throw new Error(`plugin ${unterkommando} braucht einen Plugin-Namen`);
276
+ throw new Error(`plugin ${unterkommando} braucht einen Plugin-Namen. hmgr plugin list zeigt die Plugins der bekannten Quellen.`);
276
277
  const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
277
278
  const lokal = flaggen.has('local') || flaggen.has('lokal');
278
279
  if (ziel.ort.art === 'arbeitsverzeichnis') {
@@ -292,12 +293,14 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
292
293
  console.log(` Skills: ${ergebnis.skills.join(', ')}`);
293
294
  if (ergebnis.imZeiger)
294
295
  console.log(' im Zeiger vermerkt');
296
+ // Ein Hinweis kann mehrzeilig sein, jede Zeile bekommt dieselbe Einrückung.
295
297
  if (ergebnis.hinweis !== undefined)
296
- console.log(` ${ergebnis.hinweis}`);
298
+ for (const zeile of ergebnis.hinweis.split('\n'))
299
+ console.log(` ${zeile}`);
297
300
  return 0;
298
301
  }
299
302
  if (kommando === 'sync') {
300
- const { rest } = schalterLesen(argv.slice(1));
303
+ const { rest } = schalterLesen(argv.slice(1), 'sync');
301
304
  const ergebnis = await anwendungsfaelle.abgleich(rest[0]);
302
305
  if (ergebnis.zeigerPfad !== undefined)
303
306
  console.log(`Zeiger ${ergebnis.zeigerPfad}`);
@@ -355,7 +358,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
355
358
  console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
356
359
  return 0;
357
360
  }
358
- throw new Error(`Unbekanntes Kommando „${kommando}"`);
361
+ throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
359
362
  }
360
363
  function beschreibeOrt(ort) {
361
364
  if (ort.art === 'home')
@@ -377,13 +380,43 @@ function istModellFehler(fehler) {
377
380
  fehler instanceof PluginUnbekannt ||
378
381
  fehler instanceof AbhaengigkeitFehlt);
379
382
  }
383
+ /**
384
+ * Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
385
+ * sie schreibt, die Handlungsanweisung gehört an die CLI, weil sie Kommandos nennt.
386
+ */
387
+ function naechsterSchritt(fehler) {
388
+ if (fehler instanceof SkillMehrdeutig)
389
+ return `Quelle eingrenzen: hmgr link ${fehler.name} --quelle <teilstring einer der genannten Quellen>`;
390
+ if (fehler instanceof SkillUnbekannt)
391
+ return 'Erst die Quelle hinzufügen: hmgr source add <url|pfad>. hmgr source list zeigt die bekannten Quellen.';
392
+ if (fehler instanceof NameBelegt)
393
+ return `Entweder mit --ersetzen überschreiben oder den fremden Eintrag mit hmgr adopt ${fehler.skillName} übernehmen.`;
394
+ if (fehler instanceof PluginMehrdeutig)
395
+ return 'Quelle eingrenzen: --quelle <teilstring der URL oder des Pfads>. hmgr plugin list zeigt die Marketplaces.';
396
+ if (fehler instanceof PluginUnbekannt)
397
+ return 'hmgr plugin list zeigt die Plugins der bekannten Quellen. Fehlt der Marketplace, erst hmgr source add <url>.';
398
+ if (fehler instanceof AbhaengigkeitFehlt)
399
+ return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
400
+ if (fehler instanceof KeinPassenderRef)
401
+ return 'hmgr update holt neue Tags der Quelle. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
402
+ if (fehler instanceof HarnessNichtVorhanden)
403
+ return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
404
+ if (fehler instanceof OrtNichtUnterstuetzt)
405
+ return 'profil:<name> kennt nur hermes, claude-code und agents kennen home und dir:<pfad>.';
406
+ const text = fehler instanceof Error ? fehler.message : '';
407
+ if (text.startsWith('Unbekannter Harness'))
408
+ return 'Bekannt sind claude-code, hermes und agents.';
409
+ return undefined;
410
+ }
380
411
  export async function main(argv, umgebung) {
381
412
  try {
382
413
  return await ausfuehren(argv, umgebung);
383
414
  }
384
415
  catch (fehler) {
385
416
  const text = fehler instanceof Error ? fehler.message : String(fehler);
386
- console.error(istModellFehler(fehler) ? text : `Fehler: ${text}`);
417
+ const kopf = istModellFehler(fehler) ? text : `Fehler: ${text}`;
418
+ const schritt = naechsterSchritt(fehler);
419
+ console.error(schritt === undefined ? kopf : `${kopf}\n${schritt}`);
387
420
  return 1;
388
421
  }
389
422
  }
@@ -11,6 +11,8 @@ function fehlertext(fehler) {
11
11
  }
12
12
  export class Anwendungsfaelle {
13
13
  p;
14
+ /** Schon beantwortete Orte, je Ziel der Hinweis, der davon übrig blieb (E34). */
15
+ beantwortet = new Map();
14
16
  constructor(p) {
15
17
  this.p = p;
16
18
  }
@@ -280,19 +282,47 @@ export class Anwendungsfaelle {
280
282
  const fund = await this.fundAusZeiger(args.skill, version, args.quelle);
281
283
  const link = await this.linkAnlegen(harness, args.ziel, fund, args.ersetzen);
282
284
  await this.zeigerErgaenzen(zeigerPfad, fund, version);
283
- return link;
285
+ const hinweis = await this.harnessHinweis(harness, args.ziel.ort);
286
+ return hinweis === undefined ? link : { ...link, hinweis };
284
287
  }
285
288
  /** Link legen, wenn Quelle, Ref und Skill schon feststehen. Die Zeiger-Arbeit ist dieselbe. */
286
289
  async linkAusFund(harness, ziel, fund, ersetzen, lokal) {
287
290
  const link = await this.linkAnlegen(harness, ziel, fund, ersetzen);
288
- if (lokal === true || ziel.ort.art !== 'arbeitsverzeichnis')
289
- return link;
290
- const zeigerPfad = await this.bindungFinden(ziel);
291
- // Ziel ohne Zeiger: der Link wird trotzdem gelegt, hmgr weist darauf hin (E26).
292
- if (zeigerPfad === undefined)
293
- return { ...link, hinweis: `Ohne Zeiger bleibt „${link.name}" lokal, der Link ist gelegt (E26).` };
294
- await this.zeigerErgaenzen(zeigerPfad, fund);
295
- return link;
291
+ const hinweise = [];
292
+ if (lokal !== true && ziel.ort.art === 'arbeitsverzeichnis') {
293
+ const zeigerPfad = await this.bindungFinden(ziel);
294
+ // Ziel ohne Zeiger: der Link wird trotzdem gelegt, hmgr weist darauf hin (E26).
295
+ if (zeigerPfad === undefined)
296
+ hinweise.push(`Ohne Zeiger bleibt „${link.name}" lokal, der Link ist gelegt (E26).`);
297
+ else
298
+ await this.zeigerErgaenzen(zeigerPfad, fund);
299
+ }
300
+ const vomHarness = await this.harnessHinweis(harness, ziel.ort);
301
+ if (vomHarness !== undefined)
302
+ hinweise.push(vomHarness);
303
+ return hinweise.length === 0 ? link : { ...link, hinweis: hinweise.join('\n') };
304
+ }
305
+ /**
306
+ * Offener Punkt des Harness zu einem Ort (E34), etwa fehlendes Vertrauen für Projekt-Skills. Ein
307
+ * Harness ohne diese Fähigkeit schweigt. Nennt der Harness eine Abhilfe, fragt hmgr den Nutzer:
308
+ * bei Ja leistet der Harness sie und die Ergebniszeile tritt an die Stelle des Hinweises, bei
309
+ * Nein bleibt es beim Hinweis. Je Lauf und Ziel höchstens eine Frage, die Antwort gilt für den
310
+ * ganzen Lauf.
311
+ */
312
+ async harnessHinweis(harness, ort) {
313
+ const schluessel = zielSchluessel({ harness: harness.name, ort });
314
+ if (this.beantwortet.has(schluessel))
315
+ return this.beantwortet.get(schluessel);
316
+ const punkt = await harness.hinweisNachLink?.(ort);
317
+ let hinweis = punkt?.hinweis;
318
+ const abhilfe = punkt?.abhilfe;
319
+ if (abhilfe !== undefined && (await this.p.rueckfrage.bestaetigen(abhilfe.frage))) {
320
+ await abhilfe.ausfuehren();
321
+ hinweis = abhilfe.erledigt;
322
+ }
323
+ if (punkt !== undefined)
324
+ this.beantwortet.set(schluessel, hinweis);
325
+ return hinweis;
296
326
  }
297
327
  /** Link entfernen. Ohne `lokal` verschwindet der Skill auch aus dem Zeiger (E17). */
298
328
  async linkEntfernen(args) {
@@ -325,7 +355,15 @@ export class Anwendungsfaelle {
325
355
  const skills = await this.flacheSkillListe(fund.plugin.name, fund.quelle);
326
356
  for (const skill of skills)
327
357
  await this.linkAnlegen(harness, ziel, { quelle: fund.quelle, ref: fund.ref, skill });
328
- return { plugin: fund.plugin, art: 'verlinkt', skills: skills.map((s) => s.name), imZeiger: false };
358
+ // Der Hinweis des Harness gilt dem Ort, also einmal für das ganze Plugin, nicht je Skill (E34).
359
+ const hinweis = await this.harnessHinweis(harness, ziel.ort);
360
+ return {
361
+ plugin: fund.plugin,
362
+ art: 'verlinkt',
363
+ skills: skills.map((s) => s.name),
364
+ imZeiger: false,
365
+ ...(hinweis === undefined ? {} : { hinweis }),
366
+ };
329
367
  }
330
368
  /**
331
369
  * Plugin legen (E16, E17, E24, E27). In ein Arbeitsverzeichnis mit Zeiger ist das Plugin
@@ -338,8 +376,11 @@ export class Anwendungsfaelle {
338
376
  if (args.lokal === true || args.ziel.ort.art !== 'arbeitsverzeichnis')
339
377
  return ergebnis;
340
378
  const zeigerPfad = await this.bindungFinden(args.ziel);
341
- if (zeigerPfad === undefined)
342
- return { ...ergebnis, hinweis: `Ohne Zeiger bleibt „${args.plugin}" lokal, das Plugin liegt am Ziel (E26).` };
379
+ if (zeigerPfad === undefined) {
380
+ const ohneZeiger = `Ohne Zeiger bleibt „${args.plugin}" lokal, das Plugin liegt am Ziel (E26).`;
381
+ const hinweis = ergebnis.hinweis === undefined ? ohneZeiger : `${ohneZeiger}\n${ergebnis.hinweis}`;
382
+ return { ...ergebnis, hinweis };
383
+ }
343
384
  const zeiger = await this.p.zeiger.lesen(zeigerPfad);
344
385
  const key = quelleSchluessel(fund.quelle);
345
386
  if (!zeiger.quellen.some((q) => quelleSchluessel(q) === key))
@@ -540,6 +581,10 @@ export class Anwendungsfaelle {
540
581
  continue;
541
582
  ergebnis.abweichend.push(`${harness.name}: ${name} steht nicht im Zeiger`);
542
583
  }
584
+ // Hinweis des Harness zum Ort, einmal je Harness und Lauf, nicht je Skill (E34).
585
+ const vomHarness = await this.harnessHinweis(harness, ziel.ort);
586
+ if (vomHarness !== undefined)
587
+ ergebnis.hinweise.push(vomHarness);
543
588
  }
544
589
  return ergebnis;
545
590
  }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmgr",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Harness Manager: Skills und Plugins für AI-Harnesses verwalten",
5
5
  "license": "MIT",
6
6
  "author": "Tim Kaufmann",
@@ -31,5 +31,9 @@
31
31
  "@types/node": "^24.0.0",
32
32
  "typescript": "^5.9.0",
33
33
  "vitest": "^3.2.0"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://gitea.timkaufmann.de/tim/hmgr.git"
34
38
  }
35
39
  }