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 +2 -31
- package/dist/adapter/agents.js +43 -0
- package/dist/adapter/claude-code.js +2 -1
- package/dist/adapter/hermes.js +81 -1
- package/dist/adapter/terminal.js +24 -0
- package/dist/cli/hilfe.js +254 -0
- package/dist/cli/main.js +81 -48
- package/dist/kern/anwendungsfaelle.js +57 -12
- package/dist/ports/rueckfrage.js +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -26,34 +26,5 @@ npm run build
|
|
|
26
26
|
|
|
27
27
|
## Kommandos
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
package/dist/adapter/hermes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|