hmgr 0.0.2 → 0.0.4
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/checkout-pfad.js +5 -20
- package/dist/adapter/git.js +28 -1
- package/dist/adapter/quelle-text.js +1 -3
- package/dist/adapter/terminal.js +23 -0
- package/dist/cli/hilfe.js +268 -0
- package/dist/cli/main.js +178 -52
- package/dist/kern/anwendungsfaelle.js +125 -18
- package/dist/kern/modell.js +16 -1
- package/dist/kern/quelle-ort.js +42 -0
- package/package.json +1 -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, hermes oder agents, 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.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Pfadschema der Checkouts (E22): <wurzel>/<host>/<besitzer>/<repo>/<ref>/,
|
|
2
2
|
// je Repo ein Basis-Clone unter <wurzel>/<host>/<besitzer>/<repo>/.repo.
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { ortAusSegmenten, urlOrt } from "../kern/quelle-ort.js";
|
|
4
5
|
export const BASIS_NAME = '.repo';
|
|
5
6
|
/** Refs mit „/" im Namen werden im Pfad kodiert, damit ein Ref ein Verzeichnis bleibt. */
|
|
6
7
|
export function refKodieren(ref) {
|
|
@@ -9,29 +10,13 @@ export function refKodieren(ref) {
|
|
|
9
10
|
export function refDekodieren(text) {
|
|
10
11
|
return text.replaceAll('%2F', '/').replaceAll('%25', '%');
|
|
11
12
|
}
|
|
12
|
-
function ohneGitSuffix(name) {
|
|
13
|
-
return name.endsWith('.git') ? name.slice(0, -4) : name;
|
|
14
|
-
}
|
|
15
|
-
function ausSegmenten(host, segmente) {
|
|
16
|
-
const gefiltert = segmente.filter((s) => s.length > 0);
|
|
17
|
-
const repo = ohneGitSuffix(gefiltert[gefiltert.length - 1] ?? 'repo');
|
|
18
|
-
const besitzer = gefiltert[gefiltert.length - 2] ?? '_';
|
|
19
|
-
return { host, besitzer, repo };
|
|
20
|
-
}
|
|
21
13
|
/** Zerlegt „git@host:besitzer/repo.git", „https://host/besitzer/repo", „file:///pfad" und nackte Pfade. */
|
|
22
14
|
export function urlZerlegen(url) {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
if (u.protocol === 'file:')
|
|
27
|
-
return ausSegmenten('local', decodeURIComponent(u.pathname).split('/'));
|
|
28
|
-
return ausSegmenten(u.hostname, decodeURIComponent(u.pathname).split('/'));
|
|
29
|
-
}
|
|
30
|
-
const scp = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(text);
|
|
31
|
-
if (scp)
|
|
32
|
-
return ausSegmenten(scp[1], scp[2].split('/'));
|
|
15
|
+
const ort = urlOrt(url);
|
|
16
|
+
if (ort !== undefined)
|
|
17
|
+
return ort;
|
|
33
18
|
// Nackter lokaler Pfad: Besitzer aus dem Elternverzeichnis.
|
|
34
|
-
return
|
|
19
|
+
return ortAusSegmenten('local', path.resolve(url.trim()).split(path.sep));
|
|
35
20
|
}
|
|
36
21
|
export function quelleZerlegen(quelle) {
|
|
37
22
|
return urlZerlegen(quelle.art === 'git' ? quelle.url : quelle.pfad);
|
package/dist/adapter/git.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Git über die CLI, keine Bibliothek. Checkout-Schema und Worktrees nach E22, Discovery nach E11.
|
|
2
2
|
import { execFile, spawn } from 'node:child_process';
|
|
3
|
-
import { readFile, readdir, mkdir, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { readFile, readdir, mkdir, realpath, rm, stat } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { marketplaceLesen } from "./marketplace-json.js";
|
|
@@ -225,6 +225,15 @@ export class GitQuellen {
|
|
|
225
225
|
const ausgabe = await this.git(['for-each-ref', '--format=%(refname:short)', 'refs/tags'], basis);
|
|
226
226
|
return ausgabe.split('\n').filter((z) => z.length > 0);
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Refs vom Remote nachziehen, damit eine Versionseinschränkung auch Tags sieht, die nach dem
|
|
230
|
+
* Clone entstanden sind (E33). Lokale Quellen haben keinen Clone und keinen Remote (B11).
|
|
231
|
+
*/
|
|
232
|
+
async refsNachziehen(quelle) {
|
|
233
|
+
if (quelle.art === 'lokal')
|
|
234
|
+
return;
|
|
235
|
+
await this.git(['fetch', '--prune', '--tags', 'origin'], await this.basis(quelle));
|
|
236
|
+
}
|
|
228
237
|
async standardRef(quelle) {
|
|
229
238
|
if (quelle.art === 'lokal')
|
|
230
239
|
return LOKAL_REF;
|
|
@@ -305,6 +314,24 @@ export class GitQuellen {
|
|
|
305
314
|
await this.git(['checkout', '--detach', checkout.ref], checkout.pfad);
|
|
306
315
|
return { ergebnis: 'aktualisiert', von, nach };
|
|
307
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Checkout von der Platte nehmen. Ein Worktree steht im Basis-Clone eingetragen, ein blankes
|
|
319
|
+
* Löschen des Verzeichnisses ließe den Eintrag stehen und ein späterer `worktree add` auf
|
|
320
|
+
* denselben Pfad scheiterte daran. Bleibt der Eintrag defekt, wird das Verzeichnis entfernt und
|
|
321
|
+
* der Eintrag danach mit `prune` aufgeräumt. Der Basis-Clone bleibt, er trägt alle Refs.
|
|
322
|
+
*/
|
|
323
|
+
async entfernen(checkout) {
|
|
324
|
+
if (checkout.quelle.art === 'lokal')
|
|
325
|
+
return;
|
|
326
|
+
const basis = await this.basis(checkout.quelle);
|
|
327
|
+
try {
|
|
328
|
+
await this.git(['worktree', 'remove', '--force', checkout.pfad], basis);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
await rm(checkout.pfad, { recursive: true, force: true });
|
|
332
|
+
await this.git(['worktree', 'prune'], basis);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
308
335
|
/**
|
|
309
336
|
* Arbeitskopie um einen Pfad herum (B12). Die Wurzel kommt aus `rev-parse --show-toplevel`, vom
|
|
310
337
|
* Verzeichnis des Pfads aus gefragt. Beide Pfade werden über realpath aufgelöst, sonst passen
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
export { quelleAlsText } from "../kern/modell.js";
|
|
2
3
|
/** URL heißt Git-Quelle mit Checkout, ein nackter Pfad heißt lokale Arbeitskopie ohne Clone (B11). */
|
|
3
4
|
export function quelleAusText(text) {
|
|
4
5
|
const roh = text.trim();
|
|
@@ -6,6 +7,3 @@ export function quelleAusText(text) {
|
|
|
6
7
|
return { art: 'git', url: roh };
|
|
7
8
|
return { art: 'lokal', pfad: path.resolve(roh) };
|
|
8
9
|
}
|
|
9
|
-
export function quelleAlsText(quelle) {
|
|
10
|
-
return quelle.art === 'git' ? quelle.url : quelle.pfad;
|
|
11
|
-
}
|
package/dist/adapter/terminal.js
CHANGED
|
@@ -21,4 +21,27 @@ export class TerminalRueckfrage {
|
|
|
21
21
|
leser.close();
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Nummerierte Liste, eine gelesene Zahl. Dieselbe Weiche wie bei `bestaetigen`: ohne Terminal auf
|
|
26
|
+
* beiden Seiten fragt hmgr nicht und gibt nichts aus. Alles, was keine der Nummern ist, auch die
|
|
27
|
+
* leere Eingabe, gilt als keine Antwort. Der Adapter rät nicht, er reicht das Nichts weiter.
|
|
28
|
+
*/
|
|
29
|
+
async auswaehlen(frage, optionen) {
|
|
30
|
+
if (optionen.length === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true)
|
|
33
|
+
return undefined;
|
|
34
|
+
const leser = createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
try {
|
|
36
|
+
const liste = optionen.map((option, i) => ` ${i + 1}) ${option}`).join('\n');
|
|
37
|
+
const antwort = await leser.question(`${frage}\n${liste}\nNummer [1-${optionen.length}]: `);
|
|
38
|
+
const nummer = Number(antwort.trim());
|
|
39
|
+
if (!Number.isInteger(nummer) || nummer < 1 || nummer > optionen.length)
|
|
40
|
+
return undefined;
|
|
41
|
+
return nummer - 1;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
leser.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
24
47
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
Welche Quelle gemeint ist, hängt an Host, Besitzer und Repo, nicht an der Schreibweise der URL. Ein
|
|
37
|
+
Repo in SSH- und in HTTPS-Form ist dieselbe Quelle, mit und ohne .git-Suffix ebenso. Gefetcht wird
|
|
38
|
+
unter der Schreibweise, die beim ersten source add stand.
|
|
39
|
+
|
|
40
|
+
Schalter: keine.
|
|
41
|
+
|
|
42
|
+
Beispiele:
|
|
43
|
+
hmgr source add https://github.com/tim/skills.git
|
|
44
|
+
hmgr source add ~/.skills/tim-skills
|
|
45
|
+
hmgr source list
|
|
46
|
+
|
|
47
|
+
Häufige Fälle:
|
|
48
|
+
„Skill … liegt in keiner Quelle": die Quelle fehlt, erst source add, dann link.
|
|
49
|
+
Neue Commits einer bekannten Quelle holt hmgr update, nicht source add.
|
|
50
|
+
„Quelle ist schon bekannt": dasselbe Repo steht schon im Zustand, hier in anderer Schreibweise.
|
|
51
|
+
|
|
52
|
+
Siehe auch: hmgr help link, hmgr help plugin, hmgr help update`;
|
|
53
|
+
const DIR = `hmgr dir add <pfad> [--composer <pfad>]
|
|
54
|
+
hmgr dir forget <pfad>
|
|
55
|
+
hmgr dir list
|
|
56
|
+
|
|
57
|
+
Ein Arbeitsverzeichnis ist ein Verzeichnis, in dem ein Harness gestartet wird. dir add merkt es und
|
|
58
|
+
bindet es an seinen Zeiger, also an die composer.json, in deren extra-Abschnitt Quellen, Skills und
|
|
59
|
+
Plugins des Vorhabens stehen. Ohne Zeiger bleibt das Verzeichnis gemerkt und jeder Link dort lokal
|
|
60
|
+
(E26). dir forget löst Bindung und Eintrag, die gelegten Links bleiben liegen.
|
|
61
|
+
|
|
62
|
+
Schalter:
|
|
63
|
+
--composer <pfad> nur bei add: composer.json wählen, wenn unter dem Verzeichnis mehrere in
|
|
64
|
+
Frage kommen
|
|
65
|
+
|
|
66
|
+
Beispiele:
|
|
67
|
+
hmgr dir add ~/Code/site
|
|
68
|
+
hmgr dir add ~/Code/site --composer ~/Code/site/app/composer.json
|
|
69
|
+
hmgr dir list
|
|
70
|
+
|
|
71
|
+
Häufige Fälle:
|
|
72
|
+
„Mehrere composer.json unter …": hmgr fragt am Terminal, welche es ist. Ohne Antwort, etwa in
|
|
73
|
+
einem Script, bleibt der Fehler und --composer nennt sie, hmgr rät nicht.
|
|
74
|
+
link --ort dir:<pfad> merkt das Verzeichnis von selbst. dir add braucht es vorher nur, wenn die
|
|
75
|
+
composer.json mehrdeutig ist.
|
|
76
|
+
|
|
77
|
+
Siehe auch: hmgr help link, hmgr help sync`;
|
|
78
|
+
const LINK = `hmgr link <skill> [schalter]
|
|
79
|
+
|
|
80
|
+
Legt im Ziel einen Symlink auf den Skill an, der aus einer bekannten Quelle stammt, und trägt ihn
|
|
81
|
+
in den Zeiger des Ziels ein, damit hmgr sync ihn anderswo reproduzieren kann. Einen Zeiger gibt es
|
|
82
|
+
nur für ein Arbeitsverzeichnis mit composer.json, an Home und Profil bleibt jeder Link lokal.
|
|
83
|
+
|
|
84
|
+
Schalter:
|
|
85
|
+
--harness <name> claude-code, hermes oder agents, Standard claude-code
|
|
86
|
+
--ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
|
|
87
|
+
--quelle <teil> Teilstring der Quelle, wenn der Skill-Name in mehreren Quellen vorkommt
|
|
88
|
+
--version <spanne> Versionseinschränkung nach Composer-Regeln (etwa ^1.2) oder ein Tag- oder
|
|
89
|
+
Branchname. Nur mit Zeiger, also dir:<pfad> ohne --local (E33)
|
|
90
|
+
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
91
|
+
--local Link nur ins Ziel legen, nicht in den Zeiger
|
|
92
|
+
--ersetzen einen fremden Eintrag gleichen Namens am Ziel ersetzen
|
|
93
|
+
|
|
94
|
+
Beispiele:
|
|
95
|
+
hmgr link netzwerk
|
|
96
|
+
hmgr link netzwerk --harness hermes --ort profil:researcher
|
|
97
|
+
hmgr link tq-schema --ort dir:. --version ^2.0
|
|
98
|
+
|
|
99
|
+
Häufige Fälle:
|
|
100
|
+
„liegt in mehreren Quellen": mit --quelle <teilstring der URL oder des Pfads> entscheiden.
|
|
101
|
+
„liegt bereits ein fremder Eintrag": --ersetzen setzen oder vorher hmgr adopt <name>.
|
|
102
|
+
„gibt es keinen Zeiger": --version braucht ein Arbeitsverzeichnis mit composer.json und kein
|
|
103
|
+
--local. Ohne Zeiger den Link ohne --version legen.
|
|
104
|
+
|
|
105
|
+
Siehe auch: hmgr help unlink, hmgr help sync, hmgr help adopt`;
|
|
106
|
+
const UNLINK = `hmgr unlink <skill> [schalter]
|
|
107
|
+
|
|
108
|
+
Entfernt den Link aus dem Ziel und, wenn das Ziel an einen Zeiger gebunden ist, den Skill aus dem
|
|
109
|
+
Zeiger. Der Checkout unter ~/.hmgr bleibt liegen, Links anderer Ziele bleiben unberührt.
|
|
110
|
+
|
|
111
|
+
Schalter:
|
|
112
|
+
--harness <name> claude-code, hermes oder agents, Standard claude-code
|
|
113
|
+
--ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
|
|
114
|
+
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
115
|
+
--local nur den Link entfernen, den Eintrag im Zeiger stehen lassen
|
|
116
|
+
|
|
117
|
+
Beispiele:
|
|
118
|
+
hmgr unlink netzwerk
|
|
119
|
+
hmgr unlink netzwerk --ort dir:. --local
|
|
120
|
+
|
|
121
|
+
Häufige Fälle:
|
|
122
|
+
Bleibt der Skill im Zeiger stehen, legt hmgr sync ihn wieder an. Ohne --local räumt unlink
|
|
123
|
+
beides ab.
|
|
124
|
+
|
|
125
|
+
Siehe auch: hmgr help link, hmgr help sync`;
|
|
126
|
+
const ADOPT = `hmgr adopt [name] [schalter]
|
|
127
|
+
|
|
128
|
+
Ersetzt fremde Symlinks im Ziel durch eigene hmgr-Links: hmgr liest, aus welcher Git-Arbeitskopie
|
|
129
|
+
ein Link stammt, nimmt deren remote.origin.url als Quelle, legt einen Checkout am selben Branch an
|
|
130
|
+
und verlinkt den Skill daraus. Ohne Namen laufen alle fremden Einträge des Ziels durch, mit Namen
|
|
131
|
+
nur dieser eine. Was sich nicht auflösen lässt, bleibt liegen und wird mit Grund gemeldet.
|
|
132
|
+
|
|
133
|
+
Schalter:
|
|
134
|
+
--harness <name> claude-code, hermes oder agents, Standard claude-code
|
|
135
|
+
--ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
|
|
136
|
+
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
137
|
+
--local Übernahme nicht in den Zeiger schreiben
|
|
138
|
+
--probelauf nur melden, nichts schreiben
|
|
139
|
+
|
|
140
|
+
Beispiele:
|
|
141
|
+
hmgr adopt --probelauf
|
|
142
|
+
hmgr adopt netzwerk --ort dir:.
|
|
143
|
+
|
|
144
|
+
Häufige Fälle:
|
|
145
|
+
„kein Symlink": der Eintrag ist ein echtes Verzeichnis, hmgr fasst ihn nicht an.
|
|
146
|
+
„liegt in keiner Git-Arbeitskopie" oder „ohne remote.origin.url": die Quelle ist nicht
|
|
147
|
+
bestimmbar. Mit hmgr source add <url> eine Quelle nennen und mit hmgr link --ersetzen verlinken.
|
|
148
|
+
„ohne Branch (detached HEAD)": in der Arbeitskopie einen Branch auschecken, dann erneut adopt.
|
|
149
|
+
„Pfad ist in der Quelle kein Skill": im Repo liegt an der Stelle keine SKILL.md.
|
|
150
|
+
|
|
151
|
+
Siehe auch: hmgr help link, hmgr help status`;
|
|
152
|
+
const PLUGIN = `hmgr plugin add <name> [schalter]
|
|
153
|
+
hmgr plugin remove <name> [schalter]
|
|
154
|
+
hmgr plugin list
|
|
155
|
+
|
|
156
|
+
Ein Plugin ist ein Bündel aus einem Marketplace, also aus einer Quelle mit
|
|
157
|
+
.claude-plugin/marketplace.json. Hat der Harness die Plugin-Fähigkeit (claude-code), lässt hmgr das
|
|
158
|
+
Plugin von dessen CLI installieren, sonst verlinkt es die flache Skill-Liste des Plugins samt
|
|
159
|
+
seinen Abhängigkeiten. In den Zeiger kommt das Plugin, nicht seine Einzelskills. plugin list zeigt
|
|
160
|
+
die Marketplaces aller bekannten Quellen mit ihren Plugins.
|
|
161
|
+
|
|
162
|
+
Schalter (add und remove):
|
|
163
|
+
--harness <name> claude-code, hermes oder agents, Standard claude-code
|
|
164
|
+
--ort <ort> home, dir:<pfad> oder profil:<name>, Standard home. profil kennt nur hermes
|
|
165
|
+
--composer <pfad> composer.json des Arbeitsverzeichnisses, wenn mehrere in Frage kommen
|
|
166
|
+
--local Plugin nicht in den Zeiger schreiben
|
|
167
|
+
--quelle <teil> nur bei add: Quelle eingrenzen, wenn der Name in mehreren Marketplaces steht
|
|
168
|
+
|
|
169
|
+
Beispiele:
|
|
170
|
+
hmgr plugin list
|
|
171
|
+
hmgr plugin add superpowers --ort dir:.
|
|
172
|
+
hmgr plugin remove superpowers
|
|
173
|
+
|
|
174
|
+
Häufige Fälle:
|
|
175
|
+
„Keine Quelle mit Marketplace": erst hmgr source add <url> der Quelle mit der marketplace.json.
|
|
176
|
+
„liegt in keinem Marketplace der bekannten Quellen": Namen mit hmgr plugin list prüfen.
|
|
177
|
+
„liegt in mehreren Marketplaces": mit --quelle <teilstring> entscheiden.
|
|
178
|
+
„braucht …, das im selben Marketplace nicht steht": die Abhängigkeit fehlt im Marketplace, das
|
|
179
|
+
kann nur die Quelle beheben.
|
|
180
|
+
|
|
181
|
+
Siehe auch: hmgr help source, hmgr help sync`;
|
|
182
|
+
const SYNC = `hmgr sync [pfad]
|
|
183
|
+
|
|
184
|
+
Bringt ein Arbeitsverzeichnis auf den Stand seines Zeigers: fehlende Quellen übernehmen, fehlende
|
|
185
|
+
Links legen, Links auf einen anderen Ref umlegen, Plugins herstellen. Ohne Pfad gilt das aktuelle
|
|
186
|
+
Verzeichnis. Das ist der Befehl nach einem frischen Clone eines Vorhabens und nach jeder Änderung
|
|
187
|
+
am Zeiger. Eigene Links, die im Zeiger fehlen, werden als abweichend gemeldet und nicht entfernt,
|
|
188
|
+
fremde Einträge bleiben unberührt.
|
|
189
|
+
|
|
190
|
+
Schalter: keine.
|
|
191
|
+
|
|
192
|
+
Beispiele:
|
|
193
|
+
hmgr sync
|
|
194
|
+
hmgr sync ~/Code/site
|
|
195
|
+
|
|
196
|
+
Häufige Fälle:
|
|
197
|
+
„Kein Zeiger unter …, nichts zu tun": im Verzeichnis liegt keine composer.json. Mit
|
|
198
|
+
hmgr dir add <pfad> --composer <pfad> eine nennen.
|
|
199
|
+
„steht nicht im Zeiger": den Link mit hmgr link <skill> --ort dir:<pfad> eintragen oder mit
|
|
200
|
+
hmgr unlink <skill> --ort dir:<pfad> --local entfernen.
|
|
201
|
+
„Kein Harness auf diesem Rechner": weder ~/.claude noch ~/.hermes noch ~/.agents ist angelegt.
|
|
202
|
+
|
|
203
|
+
Siehe auch: hmgr help dir, hmgr help link, hmgr help update`;
|
|
204
|
+
const UPDATE = `hmgr update
|
|
205
|
+
|
|
206
|
+
Holt für jeden Checkout unter ~/.hmgr den neuesten Stand seines Refs und zieht bei Harnesses mit
|
|
207
|
+
Plugin-Fähigkeit deren Plugins nach (E14). Die Links zeigen weiter auf dieselben Pfade und müssen
|
|
208
|
+
nicht erneuert werden. Ein Checkout mit lokaler Änderung wird übersprungen und gemeldet (E15),
|
|
209
|
+
damit nichts verloren geht. Lokale Quellen haben keinen Checkout und bleiben unberührt.
|
|
210
|
+
|
|
211
|
+
Danach räumt update verwaiste Ref-Verzeichnisse auf: Checkouts, in die kein Link mehr zeigt, weil
|
|
212
|
+
ein Link inzwischen auf einen anderen Ref umgelegt wurde. Vor dem Löschen fragt hmgr einmal für
|
|
213
|
+
alle Kandidaten. Ohne Bestätigung, also auch ohne Terminal, bleibt alles liegen und wird nur
|
|
214
|
+
gemeldet. Ein verwaister Checkout mit lokaler Änderung wird nie gelöscht (E15).
|
|
215
|
+
|
|
216
|
+
So wird eine Änderung an einem Skill überall wirksam: in der Quelle committen und pushen, dann
|
|
217
|
+
hmgr update.
|
|
218
|
+
|
|
219
|
+
Schalter: keine.
|
|
220
|
+
|
|
221
|
+
Beispiel:
|
|
222
|
+
hmgr update
|
|
223
|
+
|
|
224
|
+
Häufige Fälle:
|
|
225
|
+
„übersprungen … lokale Änderung (E15)": im Checkout liegt eine Änderung, die durch einen Link
|
|
226
|
+
hindurch gemacht wurde. Dort committen und pushen, dann erneut hmgr update.
|
|
227
|
+
„Keine Checkouts": es gibt nur lokale Quellen oder noch keinen Link.
|
|
228
|
+
„liegen lassen … ohne Bestätigung nicht gelöscht": die Frage kam ohne Ja zurück, etwa in einer
|
|
229
|
+
Pipe oder einem Composer-Script. In einem Terminal erneut hmgr update aufrufen.
|
|
230
|
+
|
|
231
|
+
Siehe auch: hmgr help sync, hmgr help status`;
|
|
232
|
+
const STATUS = `hmgr status
|
|
233
|
+
|
|
234
|
+
Liest den Ist-Zustand vom Dateisystem: je Ziel eines vorhandenen Harness die Einträge mit Quelle
|
|
235
|
+
und Ref, davor die Versionseinschränkung, falls der Zeiger eine nennt. Fremde Einträge und Links
|
|
236
|
+
ohne bekannte Quelle sind eigens gekennzeichnet, dazu kommen die Plugins des Ziels. Am Ende stehen
|
|
237
|
+
die Harnesses, die auf diesem Rechner nicht liegen.
|
|
238
|
+
|
|
239
|
+
Schalter: keine.
|
|
240
|
+
|
|
241
|
+
Beispiel:
|
|
242
|
+
hmgr status
|
|
243
|
+
|
|
244
|
+
Häufige Fälle:
|
|
245
|
+
„fremd": der Eintrag stammt nicht von hmgr. hmgr adopt <name> übernimmt ihn.
|
|
246
|
+
„Link ohne bekannte Quelle": der Link zeigt in einen hmgr-Checkout, dessen Quelle nicht mehr im
|
|
247
|
+
Zustand steht. Mit hmgr source add <url> die Quelle wieder eintragen.
|
|
248
|
+
„Nicht vorhanden": der Harness hat sein Verzeichnis auf diesem Rechner nicht angelegt, hmgr
|
|
249
|
+
überspringt seine Ziele.
|
|
250
|
+
|
|
251
|
+
Siehe auch: hmgr help adopt, hmgr help sync`;
|
|
252
|
+
/** Ebene 2, je Kommandofamilie ein Text. Unterkommandos teilen sich die Hilfe ihrer Familie. */
|
|
253
|
+
const THEMEN = new Map([
|
|
254
|
+
['source', SOURCE],
|
|
255
|
+
['dir', DIR],
|
|
256
|
+
['link', LINK],
|
|
257
|
+
['unlink', UNLINK],
|
|
258
|
+
['adopt', ADOPT],
|
|
259
|
+
['plugin', PLUGIN],
|
|
260
|
+
['sync', SYNC],
|
|
261
|
+
['update', UPDATE],
|
|
262
|
+
['status', STATUS],
|
|
263
|
+
['help', HILFE],
|
|
264
|
+
]);
|
|
265
|
+
/** Hilfetext zu einem Kommando, undefined für ein unbekanntes Thema. */
|
|
266
|
+
export function hilfeText(thema) {
|
|
267
|
+
return THEMEN.get(thema);
|
|
268
|
+
}
|
package/dist/cli/main.js
CHANGED
|
@@ -15,6 +15,7 @@ import { GitQuellen } from "../adapter/git.js";
|
|
|
15
15
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
16
16
|
import { TerminalRueckfrage } from "../adapter/terminal.js";
|
|
17
17
|
import { ZustandJson } from "../adapter/zustand-json.js";
|
|
18
|
+
import { HILFE, hilfeText } from "./hilfe.js";
|
|
18
19
|
export function umgebungLesen(env = process.env) {
|
|
19
20
|
const home = env.HOME ?? homedir();
|
|
20
21
|
return { home, hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr') };
|
|
@@ -35,9 +36,44 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
35
36
|
};
|
|
36
37
|
return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Was jedes Kommando auswertet, gleichlautend mit seiner Hilfe. Alles andere bricht ab. `--help`
|
|
41
|
+
* steht nirgends, weil der Hilfe-Dispatch vor jedem schalterLesen läuft.
|
|
42
|
+
*/
|
|
43
|
+
const SCHALTER = {
|
|
44
|
+
source: { werte: [], flaggen: [] },
|
|
45
|
+
dir: { werte: ['composer'], flaggen: [] },
|
|
46
|
+
link: {
|
|
47
|
+
werte: ['harness', 'ort', 'composer', 'quelle', 'version'],
|
|
48
|
+
flaggen: ['local', 'lokal', 'ersetzen'],
|
|
49
|
+
},
|
|
50
|
+
unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
|
|
51
|
+
adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
|
|
52
|
+
plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
|
|
53
|
+
sync: { werte: [], flaggen: [] },
|
|
54
|
+
update: { werte: [], flaggen: [] },
|
|
55
|
+
status: { werte: [], flaggen: [] },
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Was ein einzelnes Unterkommando von seiner Familie liest, gleichlautend mit ihrer Hilfe. Ohne
|
|
59
|
+
* `schalter` bleibt es bei der Familienliste, das reicht, solange alle Unterkommandos dieselben
|
|
60
|
+
* Schalter lesen.
|
|
61
|
+
*/
|
|
62
|
+
const UNTERKOMMANDOS = {
|
|
63
|
+
source: { add: { woerter: 2 }, list: { woerter: 1 } },
|
|
64
|
+
dir: {
|
|
65
|
+
add: { woerter: 2, schalter: ['composer'] },
|
|
66
|
+
forget: { woerter: 2, schalter: [] },
|
|
67
|
+
list: { woerter: 1, schalter: [] },
|
|
68
|
+
},
|
|
69
|
+
plugin: {
|
|
70
|
+
add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle', 'local', 'lokal'] },
|
|
71
|
+
remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
|
|
72
|
+
list: { woerter: 1, schalter: [] },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
function schalterLesen(args, kommando) {
|
|
76
|
+
const erlaubt = SCHALTER[kommando];
|
|
41
77
|
const rest = [];
|
|
42
78
|
const werte = new Map();
|
|
43
79
|
const flaggen = new Set();
|
|
@@ -48,17 +84,50 @@ function schalterLesen(args) {
|
|
|
48
84
|
continue;
|
|
49
85
|
}
|
|
50
86
|
const name = arg.slice(2);
|
|
51
|
-
if (
|
|
87
|
+
if (erlaubt.flaggen.includes(name)) {
|
|
52
88
|
flaggen.add(name);
|
|
53
89
|
continue;
|
|
54
90
|
}
|
|
91
|
+
if (!erlaubt.werte.includes(name))
|
|
92
|
+
throw new Error(`Unbekannter Schalter --${name} für ${kommando}. hmgr help ${kommando} zeigt die Schalter.`);
|
|
55
93
|
const wert = args[++i];
|
|
56
94
|
if (wert === undefined)
|
|
57
|
-
throw new Error(`Schalter --${name} braucht einen Wert`
|
|
95
|
+
throw new Error(`Schalter --${name} braucht einen Wert, etwa --${name} <wert>. ` +
|
|
96
|
+
`hmgr help ${kommando} zeigt die Schalter.`);
|
|
58
97
|
werte.set(name, wert);
|
|
59
98
|
}
|
|
60
99
|
return { rest, werte, flaggen };
|
|
61
100
|
}
|
|
101
|
+
/** Ein Wort mehr, als das Kommando liest, ist ein Tippfehler und kein stiller Zusatz. */
|
|
102
|
+
function restPruefen(rest, woerter, kommando, unterkommando) {
|
|
103
|
+
const zuviel = rest[woerter];
|
|
104
|
+
if (zuviel === undefined)
|
|
105
|
+
return;
|
|
106
|
+
const verwendung = unterkommando === undefined ? kommando : `${kommando} ${unterkommando}`;
|
|
107
|
+
// Der Name des Unterkommandos zählt zu den Wörtern, aber nicht zu den Argumenten.
|
|
108
|
+
const argumente = woerter - (unterkommando === undefined ? 0 : 1);
|
|
109
|
+
throw new Error(`${verwendung} erwartet ${argumente === 0 ? 'kein' : 'kein weiteres'} Argument „${zuviel}". ` +
|
|
110
|
+
`hmgr help ${kommando} zeigt die Verwendung.`);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Prüft, was erst mit dem Unterkommando feststeht: liest es die gesetzten Schalter, und steht ein
|
|
114
|
+
* Wort zu viel da. Ein unbekanntes Unterkommando meldet die Familie selbst.
|
|
115
|
+
*/
|
|
116
|
+
function unterkommandoPruefen(kommando, schalter) {
|
|
117
|
+
const unterkommando = schalter.rest[0];
|
|
118
|
+
if (unterkommando === undefined)
|
|
119
|
+
return;
|
|
120
|
+
const menge = UNTERKOMMANDOS[kommando][unterkommando];
|
|
121
|
+
if (menge === undefined)
|
|
122
|
+
return;
|
|
123
|
+
const erlaubt = menge.schalter;
|
|
124
|
+
if (erlaubt !== undefined)
|
|
125
|
+
for (const name of [...schalter.werte.keys(), ...schalter.flaggen])
|
|
126
|
+
if (!erlaubt.includes(name))
|
|
127
|
+
throw new Error(`Unbekannter Schalter --${name} für ${kommando} ${unterkommando}. ` +
|
|
128
|
+
`hmgr help ${kommando} zeigt die Schalter.`);
|
|
129
|
+
restPruefen(schalter.rest, menge.woerter, kommando, unterkommando);
|
|
130
|
+
}
|
|
62
131
|
function ortLesen(text) {
|
|
63
132
|
const roh = text ?? 'home';
|
|
64
133
|
if (roh === 'home')
|
|
@@ -69,31 +138,6 @@ function ortLesen(text) {
|
|
|
69
138
|
return { art: 'profil', name: roh.slice(7) };
|
|
70
139
|
throw new Error(`Unbekannter Ort „${roh}", erwartet home, dir:<pfad> oder profil:<name>`);
|
|
71
140
|
}
|
|
72
|
-
const HILFE = `hmgr <kommando>
|
|
73
|
-
|
|
74
|
-
source add <url|pfad> Quelle hinzufügen (URL wird geklont, Pfad bleibt Arbeitskopie)
|
|
75
|
-
source list Quellen zeigen
|
|
76
|
-
dir add <pfad> Arbeitsverzeichnis merken und an seinen Zeiger binden
|
|
77
|
-
dir forget <pfad> Arbeitsverzeichnis und Bindung vergessen, Links bleiben
|
|
78
|
-
dir list Arbeitsverzeichnisse mit ihrer Bindung zeigen
|
|
79
|
-
link <skill> Skill in ein Ziel verlinken
|
|
80
|
-
unlink <skill> Link entfernen
|
|
81
|
-
adopt [name] Fremde Links im Ziel durch eigene hmgr-Links ersetzen
|
|
82
|
-
plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
|
|
83
|
-
plugin remove <name> Plugin aus einem Ziel entfernen
|
|
84
|
-
plugin list Plugins der Marketplaces aller Quellen
|
|
85
|
-
sync [pfad] Ziel auf den Stand seines Zeigers bringen
|
|
86
|
-
update Alle Checkouts aktualisieren
|
|
87
|
-
status Ist-Zustand der Ziele
|
|
88
|
-
|
|
89
|
-
--harness <name> claude-code, hermes oder agents, Standard claude-code
|
|
90
|
-
--ort home|dir:<pfad>|profil:<name> Standard home
|
|
91
|
-
--quelle <teilstring> Quelle bei mehrdeutigem Skill-Namen
|
|
92
|
-
--version <spanne> Versionseinschränkung nach Composer-Regeln, nur mit Zeiger (E33)
|
|
93
|
-
--composer <pfad> composer.json wählen, wenn mehrere in Frage kommen
|
|
94
|
-
--local Link nur ins Ziel legen, nicht in den Zeiger
|
|
95
|
-
--ersetzen fremden Eintrag am Ziel ersetzen
|
|
96
|
-
--probelauf bei adopt nur melden, nichts schreiben`;
|
|
97
141
|
/** Klartext für den Grund, aus dem ein fremder Eintrag liegen bleibt (B12). */
|
|
98
142
|
const UEBERNAHME_GRUND = {
|
|
99
143
|
'kein-link': 'kein Symlink',
|
|
@@ -105,18 +149,46 @@ const UEBERNAHME_GRUND = {
|
|
|
105
149
|
export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
106
150
|
const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
|
|
107
151
|
const kommando = argv[0];
|
|
108
|
-
|
|
152
|
+
// Hilfe zuerst: --help ist überall eine Flagge, damit kein Kommando sie als Wert liest.
|
|
153
|
+
if (kommando === undefined) {
|
|
109
154
|
console.log(HILFE);
|
|
110
|
-
return
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
if (kommando === 'help' || kommando === '--help' || kommando === '-h') {
|
|
158
|
+
const thema = argv[1];
|
|
159
|
+
if (thema === undefined || thema.startsWith('-')) {
|
|
160
|
+
console.log(HILFE);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
const text = hilfeText(thema);
|
|
164
|
+
if (text === undefined)
|
|
165
|
+
throw new Error(`Unbekanntes Hilfe-Thema „${thema}". hmgr --help zeigt alle Kommandos.`);
|
|
166
|
+
console.log(text);
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
if (argv.slice(1).includes('--help')) {
|
|
170
|
+
const text = hilfeText(kommando);
|
|
171
|
+
if (text === undefined)
|
|
172
|
+
throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
|
|
173
|
+
console.log(text);
|
|
174
|
+
return 0;
|
|
111
175
|
}
|
|
112
176
|
if (kommando === 'source') {
|
|
113
|
-
const
|
|
177
|
+
const schalter = schalterLesen(argv.slice(1), 'source');
|
|
178
|
+
unterkommandoPruefen('source', schalter);
|
|
179
|
+
const rest = schalter.rest;
|
|
180
|
+
const unterkommando = rest[0];
|
|
114
181
|
if (unterkommando === 'add') {
|
|
115
|
-
const text =
|
|
182
|
+
const text = rest[1];
|
|
116
183
|
if (text === undefined)
|
|
117
|
-
throw new Error('source add braucht eine URL oder einen Pfad');
|
|
118
|
-
const
|
|
119
|
-
const { skills, marketplace } = await anwendungsfaelle.quelleHinzufuegen(
|
|
184
|
+
throw new Error('source add braucht eine URL oder einen Pfad, etwa hmgr source add https://example.com/skills.git');
|
|
185
|
+
const genannt = quelleAusText(text);
|
|
186
|
+
const { quelle, skills, marketplace, bekannt } = await anwendungsfaelle.quelleHinzufuegen(genannt);
|
|
187
|
+
// Dieselbe Quelle in anderer Schreibweise legt keine zweite an (E36).
|
|
188
|
+
if (bekannt)
|
|
189
|
+
console.log(quelleAlsText(quelle) === quelleAlsText(genannt)
|
|
190
|
+
? 'Quelle ist schon bekannt, keine zweite angelegt.'
|
|
191
|
+
: `Quelle ist schon bekannt, geführt als ${quelleAlsText(quelle)}. Keine zweite angelegt.`);
|
|
120
192
|
console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}), ${skills.length} Skills:`);
|
|
121
193
|
for (const skill of skills)
|
|
122
194
|
console.log(` ${skill.name} ${skill.pfad}`);
|
|
@@ -137,15 +209,17 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
137
209
|
console.log(`${quelle.art} ${quelleAlsText(quelle)}`);
|
|
138
210
|
return 0;
|
|
139
211
|
}
|
|
140
|
-
throw new Error('source add <url|pfad> oder source list
|
|
212
|
+
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.`);
|
|
141
213
|
}
|
|
142
214
|
if (kommando === 'dir') {
|
|
143
|
-
const
|
|
215
|
+
const schalter = schalterLesen(argv.slice(1), 'dir');
|
|
216
|
+
unterkommandoPruefen('dir', schalter);
|
|
217
|
+
const { rest, werte } = schalter;
|
|
144
218
|
const unterkommando = rest[0];
|
|
145
219
|
if (unterkommando === 'add') {
|
|
146
220
|
const pfad = rest[1];
|
|
147
221
|
if (pfad === undefined)
|
|
148
|
-
throw new Error('dir add braucht einen Pfad');
|
|
222
|
+
throw new Error('dir add braucht einen Pfad, etwa hmgr dir add .');
|
|
149
223
|
const composer = werte.get('composer');
|
|
150
224
|
const ergebnis = composer === undefined
|
|
151
225
|
? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)
|
|
@@ -160,7 +234,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
160
234
|
if (unterkommando === 'forget') {
|
|
161
235
|
const pfad = rest[1];
|
|
162
236
|
if (pfad === undefined)
|
|
163
|
-
throw new Error('dir forget braucht einen Pfad');
|
|
237
|
+
throw new Error('dir forget braucht einen Pfad. hmgr dir list zeigt die gemerkten Arbeitsverzeichnisse.');
|
|
164
238
|
await anwendungsfaelle.arbeitsverzeichnisVergessen(pfad);
|
|
165
239
|
console.log(`Arbeitsverzeichnis ${path.resolve(pfad)} vergessen, die Links bleiben liegen.`);
|
|
166
240
|
return 0;
|
|
@@ -175,13 +249,15 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
175
249
|
}
|
|
176
250
|
return 0;
|
|
177
251
|
}
|
|
178
|
-
throw new Error('dir add <pfad>, dir forget <pfad> oder dir list
|
|
252
|
+
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.`);
|
|
179
253
|
}
|
|
180
254
|
if (kommando === 'link' || kommando === 'unlink') {
|
|
181
|
-
const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
|
|
255
|
+
const { rest, werte, flaggen } = schalterLesen(argv.slice(1), kommando);
|
|
256
|
+
restPruefen(rest, 1, kommando);
|
|
182
257
|
const skill = rest[0];
|
|
183
258
|
if (skill === undefined)
|
|
184
|
-
throw new Error(`${kommando} braucht einen Skill-Namen`
|
|
259
|
+
throw new Error(`${kommando} braucht einen Skill-Namen, etwa hmgr ${kommando} <skill>. ` +
|
|
260
|
+
'hmgr status zeigt die verlinkten Skills, hmgr source add <url|pfad> die Skills einer Quelle.');
|
|
185
261
|
const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
|
|
186
262
|
const lokal = flaggen.has('local') || flaggen.has('lokal');
|
|
187
263
|
if (ziel.ort.art === 'arbeitsverzeichnis') {
|
|
@@ -214,7 +290,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
214
290
|
return 0;
|
|
215
291
|
}
|
|
216
292
|
if (kommando === 'adopt') {
|
|
217
|
-
const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
|
|
293
|
+
const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'adopt');
|
|
294
|
+
restPruefen(rest, 1, 'adopt');
|
|
218
295
|
const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
|
|
219
296
|
const probelauf = flaggen.has('probelauf');
|
|
220
297
|
const name = rest[0];
|
|
@@ -253,7 +330,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
253
330
|
return 0;
|
|
254
331
|
}
|
|
255
332
|
if (kommando === 'plugin') {
|
|
256
|
-
const
|
|
333
|
+
const schalter = schalterLesen(argv.slice(1), 'plugin');
|
|
334
|
+
unterkommandoPruefen('plugin', schalter);
|
|
335
|
+
const { rest, werte, flaggen } = schalter;
|
|
257
336
|
const unterkommando = rest[0];
|
|
258
337
|
if (unterkommando === 'list') {
|
|
259
338
|
const marketplaces = await anwendungsfaelle.marketplaces();
|
|
@@ -272,10 +351,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
272
351
|
return 0;
|
|
273
352
|
}
|
|
274
353
|
if (unterkommando !== 'add' && unterkommando !== 'remove')
|
|
275
|
-
throw new Error('plugin add <name>, plugin remove <name> oder plugin list
|
|
354
|
+
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.`);
|
|
276
355
|
const name = rest[1];
|
|
277
356
|
if (name === undefined)
|
|
278
|
-
throw new Error(`plugin ${unterkommando} braucht einen Plugin-Namen
|
|
357
|
+
throw new Error(`plugin ${unterkommando} braucht einen Plugin-Namen. hmgr plugin list zeigt die Plugins der bekannten Quellen.`);
|
|
279
358
|
const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
|
|
280
359
|
const lokal = flaggen.has('local') || flaggen.has('lokal');
|
|
281
360
|
if (ziel.ort.art === 'arbeitsverzeichnis') {
|
|
@@ -302,7 +381,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
302
381
|
return 0;
|
|
303
382
|
}
|
|
304
383
|
if (kommando === 'sync') {
|
|
305
|
-
const { rest } = schalterLesen(argv.slice(1));
|
|
384
|
+
const { rest } = schalterLesen(argv.slice(1), 'sync');
|
|
385
|
+
restPruefen(rest, 1, 'sync');
|
|
306
386
|
const ergebnis = await anwendungsfaelle.abgleich(rest[0]);
|
|
307
387
|
if (ergebnis.zeigerPfad !== undefined)
|
|
308
388
|
console.log(`Zeiger ${ergebnis.zeigerPfad}`);
|
|
@@ -319,7 +399,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
319
399
|
return 0;
|
|
320
400
|
}
|
|
321
401
|
if (kommando === 'update') {
|
|
322
|
-
|
|
402
|
+
restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
|
|
403
|
+
const { checkouts: liste, pluginHarnesses, aufraeumen } = await anwendungsfaelle.aktualisieren();
|
|
323
404
|
if (liste.length === 0)
|
|
324
405
|
console.log('Keine Checkouts.');
|
|
325
406
|
for (const { checkout, aktualisierung } of liste) {
|
|
@@ -333,9 +414,24 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
333
414
|
}
|
|
334
415
|
for (const name of pluginHarnesses)
|
|
335
416
|
console.log(` Plugins ${name} aktualisiert (E14)`);
|
|
417
|
+
// Zum Schluss die verwaisten Checkouts: was gelöscht wurde und was liegen bleibt.
|
|
418
|
+
for (const verwaist of aufraeumen.verwaiste) {
|
|
419
|
+
const kopf = `${quelleAlsText(verwaist.checkout.quelle)} @ ${verwaist.checkout.ref}`;
|
|
420
|
+
if (verwaist.geloescht)
|
|
421
|
+
console.log(` gelöscht ${kopf} verwaist (${verwaist.checkout.pfad})`);
|
|
422
|
+
else if (verwaist.lokaleAenderung)
|
|
423
|
+
console.log(` liegen lassen ${kopf} verwaist, aber lokale Änderung (E15)`);
|
|
424
|
+
else
|
|
425
|
+
console.log(` liegen lassen ${kopf} verwaist, ohne Bestätigung nicht gelöscht`);
|
|
426
|
+
}
|
|
427
|
+
if (aufraeumen.verwaiste.length > 0) {
|
|
428
|
+
const geloescht = aufraeumen.verwaiste.filter((v) => v.geloescht).length;
|
|
429
|
+
console.log(`${aufraeumen.verwaiste.length} verwaist, ${geloescht} gelöscht, ${aufraeumen.verwaiste.length - geloescht} liegen geblieben.`);
|
|
430
|
+
}
|
|
336
431
|
return 0;
|
|
337
432
|
}
|
|
338
433
|
if (kommando === 'status') {
|
|
434
|
+
restPruefen(schalterLesen(argv.slice(1), 'status').rest, 0, 'status');
|
|
339
435
|
for (const zielZustand of await anwendungsfaelle.istZustand()) {
|
|
340
436
|
console.log(`Ziel ${zielZustand.ziel.harness} ${beschreibeOrt(zielZustand.ziel.ort)} (${zielZustand.verzeichnis})`);
|
|
341
437
|
if (zielZustand.eintraege.length === 0)
|
|
@@ -360,7 +456,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
360
456
|
console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
|
|
361
457
|
return 0;
|
|
362
458
|
}
|
|
363
|
-
throw new Error(`Unbekanntes Kommando „${kommando}"
|
|
459
|
+
throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
|
|
364
460
|
}
|
|
365
461
|
function beschreibeOrt(ort) {
|
|
366
462
|
if (ort.art === 'home')
|
|
@@ -382,13 +478,43 @@ function istModellFehler(fehler) {
|
|
|
382
478
|
fehler instanceof PluginUnbekannt ||
|
|
383
479
|
fehler instanceof AbhaengigkeitFehlt);
|
|
384
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
|
|
483
|
+
* sie schreibt, die Handlungsanweisung gehört an die CLI, weil sie Kommandos nennt.
|
|
484
|
+
*/
|
|
485
|
+
function naechsterSchritt(fehler) {
|
|
486
|
+
if (fehler instanceof SkillMehrdeutig)
|
|
487
|
+
return `Quelle eingrenzen: hmgr link ${fehler.name} --quelle <teilstring einer der genannten Quellen>`;
|
|
488
|
+
if (fehler instanceof SkillUnbekannt)
|
|
489
|
+
return 'Erst die Quelle hinzufügen: hmgr source add <url|pfad>. hmgr source list zeigt die bekannten Quellen.';
|
|
490
|
+
if (fehler instanceof NameBelegt)
|
|
491
|
+
return `Entweder mit --ersetzen überschreiben oder den fremden Eintrag mit hmgr adopt ${fehler.skillName} übernehmen.`;
|
|
492
|
+
if (fehler instanceof PluginMehrdeutig)
|
|
493
|
+
return 'Quelle eingrenzen: --quelle <teilstring der URL oder des Pfads>. hmgr plugin list zeigt die Marketplaces.';
|
|
494
|
+
if (fehler instanceof PluginUnbekannt)
|
|
495
|
+
return 'hmgr plugin list zeigt die Plugins der bekannten Quellen. Fehlt der Marketplace, erst hmgr source add <url>.';
|
|
496
|
+
if (fehler instanceof AbhaengigkeitFehlt)
|
|
497
|
+
return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
|
|
498
|
+
if (fehler instanceof KeinPassenderRef)
|
|
499
|
+
return 'Die Tags der Quelle sind frisch geholt, keiner passt. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
|
|
500
|
+
if (fehler instanceof HarnessNichtVorhanden)
|
|
501
|
+
return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
|
|
502
|
+
if (fehler instanceof OrtNichtUnterstuetzt)
|
|
503
|
+
return 'profil:<name> kennt nur hermes, claude-code und agents kennen home und dir:<pfad>.';
|
|
504
|
+
const text = fehler instanceof Error ? fehler.message : '';
|
|
505
|
+
if (text.startsWith('Unbekannter Harness'))
|
|
506
|
+
return 'Bekannt sind claude-code, hermes und agents.';
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
385
509
|
export async function main(argv, umgebung) {
|
|
386
510
|
try {
|
|
387
511
|
return await ausfuehren(argv, umgebung);
|
|
388
512
|
}
|
|
389
513
|
catch (fehler) {
|
|
390
514
|
const text = fehler instanceof Error ? fehler.message : String(fehler);
|
|
391
|
-
|
|
515
|
+
const kopf = istModellFehler(fehler) ? text : `Fehler: ${text}`;
|
|
516
|
+
const schritt = naechsterSchritt(fehler);
|
|
517
|
+
console.error(schritt === undefined ? kopf : `${kopf}\n${schritt}`);
|
|
392
518
|
return 1;
|
|
393
519
|
}
|
|
394
520
|
}
|
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
// Anwendungsfälle. Pfade werden hier nur als Zeichenketten normalisiert, gelesen wird über die Ports.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
3
|
+
import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, marketplaceUrl, pluginSchluessel, pluginSchluesselZerlegen, quelleAlsText, quelleSchluessel, zielSchluessel, } from "./modell.js";
|
|
4
4
|
import { passenderRef } from "./version.js";
|
|
5
5
|
import { kannPlugins, kannProfile } from "../ports/harness.js";
|
|
6
|
+
/**
|
|
7
|
+
* Frage vor dem Löschen, eine für alle Kandidaten (E28). Der Kern formuliert sie fertig, der
|
|
8
|
+
* Adapter stellt sie: Anzahl und Pfade, damit der Nutzer sieht, worüber er entscheidet.
|
|
9
|
+
*/
|
|
10
|
+
export function aufraeumFrage(checkouts) {
|
|
11
|
+
const kopf = checkouts.length === 1
|
|
12
|
+
? 'Ein Checkout ist verwaist, kein Link zeigt hinein:'
|
|
13
|
+
: `${checkouts.length} Checkouts sind verwaist, kein Link zeigt hinein:`;
|
|
14
|
+
const pfade = checkouts.map((c) => ` ${c.pfad}`).join('\n');
|
|
15
|
+
return `${kopf}\n${pfade}\nLöschen? [j/N]`;
|
|
16
|
+
}
|
|
6
17
|
function arbeitsverzeichnisZiel(harness, pfad) {
|
|
7
18
|
return { harness, ort: { art: 'arbeitsverzeichnis', pfad } };
|
|
8
19
|
}
|
|
9
20
|
function fehlertext(fehler) {
|
|
10
21
|
return fehler instanceof Error ? fehler.message : String(fehler);
|
|
11
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Quellenhinweis bei Mehrdeutigkeit (E12): Teilstring des Schlüssels oder der Schreibweise. Beides
|
|
25
|
+
* zählt, damit `host/besitzer/repo` genauso greift wie ein Stück der URL, unter der die Quelle
|
|
26
|
+
* geführt wird (E36).
|
|
27
|
+
*/
|
|
28
|
+
function quellePasst(quelle, hinweis) {
|
|
29
|
+
if (hinweis === undefined)
|
|
30
|
+
return true;
|
|
31
|
+
return quelleSchluessel(quelle).includes(hinweis) || quelleAlsText(quelle).includes(hinweis);
|
|
32
|
+
}
|
|
12
33
|
export class Anwendungsfaelle {
|
|
13
34
|
p;
|
|
14
35
|
/** Schon beantwortete Orte, je Ziel der Hinweis, der davon übrig blieb (E34). */
|
|
15
36
|
beantwortet = new Map();
|
|
37
|
+
/** Quellen, deren Refs in diesem Lauf schon nachgezogen wurden (E33). */
|
|
38
|
+
nachgezogen = new Set();
|
|
16
39
|
constructor(p) {
|
|
17
40
|
this.p = p;
|
|
18
41
|
}
|
|
@@ -45,18 +68,25 @@ export class Anwendungsfaelle {
|
|
|
45
68
|
ergebnis.push(harness.name);
|
|
46
69
|
return ergebnis;
|
|
47
70
|
}
|
|
48
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Quelle hinzufügen: prüft, dass sie erreichbar ist, und liest Skills und Marketplace (E24).
|
|
73
|
+
* Steht dieselbe Quelle schon in einer anderen Schreibweise im Zustand, kommt keine zweite dazu
|
|
74
|
+
* (E36). Gelesen wird dann unter der Schreibweise, unter der sie geführt wird, denn mit ihr ist
|
|
75
|
+
* der Basis-Clone angelegt. `bekannt` sagt, ob es so kam, `quelle` nennt die geführte Fassung.
|
|
76
|
+
*/
|
|
49
77
|
async quelleHinzufuegen(quelle) {
|
|
50
|
-
const ref = await this.p.quellen.standardRef(quelle);
|
|
51
|
-
const skills = await this.p.quellen.skills(quelle, ref);
|
|
52
|
-
const marketplace = await this.p.quellen.marketplace(quelle, ref);
|
|
53
78
|
const zustand = await this.p.zustand.lesen();
|
|
54
79
|
const key = quelleSchluessel(quelle);
|
|
55
|
-
|
|
80
|
+
const gefuehrt = zustand.quellen.find((q) => quelleSchluessel(q) === key);
|
|
81
|
+
const wirksam = gefuehrt ?? quelle;
|
|
82
|
+
const ref = await this.p.quellen.standardRef(wirksam);
|
|
83
|
+
const skills = await this.p.quellen.skills(wirksam, ref);
|
|
84
|
+
const marketplace = await this.p.quellen.marketplace(wirksam, ref);
|
|
85
|
+
if (gefuehrt === undefined) {
|
|
56
86
|
zustand.quellen.push(quelle);
|
|
57
87
|
await this.p.zustand.schreiben(zustand);
|
|
58
88
|
}
|
|
59
|
-
return { skills, marketplace };
|
|
89
|
+
return { quelle: wirksam, skills, marketplace, bekannt: gefuehrt !== undefined };
|
|
60
90
|
}
|
|
61
91
|
/** Marketplace einer Quelle am Standard-Ref, mit den Plugins als Modellwerte. */
|
|
62
92
|
async marketplaceLesen(quelle) {
|
|
@@ -95,7 +125,7 @@ export class Anwendungsfaelle {
|
|
|
95
125
|
const zustand = await this.p.zustand.lesen();
|
|
96
126
|
const funde = [];
|
|
97
127
|
for (const quelle of zustand.quellen) {
|
|
98
|
-
if (
|
|
128
|
+
if (!quellePasst(quelle, quelleHinweis))
|
|
99
129
|
continue;
|
|
100
130
|
const gelesen = await this.marketplaceLesen(quelle).catch(() => null);
|
|
101
131
|
if (gelesen === null)
|
|
@@ -168,7 +198,7 @@ export class Anwendungsfaelle {
|
|
|
168
198
|
if (fund.ergebnis === 'gefunden')
|
|
169
199
|
zeigerPfad = fund.pfad;
|
|
170
200
|
else if (fund.ergebnis === 'mehrdeutig')
|
|
171
|
-
|
|
201
|
+
zeigerPfad = await this.zeigerWaehlen(verzeichnis, fund.kandidaten);
|
|
172
202
|
else
|
|
173
203
|
hinweis = `Kein Zeiger unter ${verzeichnis}, alles bleibt lokal (E26).`;
|
|
174
204
|
}
|
|
@@ -201,6 +231,20 @@ export class Anwendungsfaelle {
|
|
|
201
231
|
zustand.bindungen = zustand.bindungen.filter((b) => !(b.ziel.ort.art === 'arbeitsverzeichnis' && b.ziel.ort.pfad === verzeichnis));
|
|
202
232
|
await this.p.zustand.schreiben(zustand);
|
|
203
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Mehrere composer.json unter einem Verzeichnis (E26): hmgr rät nicht, es fragt. Bleibt die Frage
|
|
236
|
+
* ohne Antwort, weil niemand antworten kann oder die Eingabe keine der Kandidaten trifft, gilt
|
|
237
|
+
* wie bisher der Fehler mit dem Weg über --composer. Der Fragetext kommt von hier, der Port kennt
|
|
238
|
+
* kein Terminal.
|
|
239
|
+
*/
|
|
240
|
+
async zeigerWaehlen(verzeichnis, kandidaten) {
|
|
241
|
+
const frage = `Mehrere composer.json unter ${verzeichnis}. Welche ist der Zeiger?`;
|
|
242
|
+
const gewaehlt = await this.p.rueckfrage.auswaehlen(frage, kandidaten);
|
|
243
|
+
const pfad = gewaehlt === undefined ? undefined : kandidaten[gewaehlt];
|
|
244
|
+
if (pfad === undefined)
|
|
245
|
+
throw new ZeigerMehrdeutig(verzeichnis, kandidaten);
|
|
246
|
+
return pfad;
|
|
247
|
+
}
|
|
204
248
|
/** Zeiger eines Arbeitsverzeichnisses aus dem Zustand, egal für welchen Harness (E26). */
|
|
205
249
|
async bindungFuerVerzeichnis(verzeichnis) {
|
|
206
250
|
const zustand = await this.p.zustand.lesen();
|
|
@@ -217,7 +261,7 @@ export class Anwendungsfaelle {
|
|
|
217
261
|
const zustand = await this.p.zustand.lesen();
|
|
218
262
|
const funde = [];
|
|
219
263
|
for (const quelle of zustand.quellen) {
|
|
220
|
-
if (
|
|
264
|
+
if (!quellePasst(quelle, quelleHinweis))
|
|
221
265
|
continue;
|
|
222
266
|
const ref = await this.p.quellen.standardRef(quelle);
|
|
223
267
|
const skills = await this.p.quellen.skills(quelle, ref);
|
|
@@ -457,6 +501,19 @@ export class Anwendungsfaelle {
|
|
|
457
501
|
await this.p.zeiger.schreiben(zeigerPfad, zeiger);
|
|
458
502
|
return { ...ergebnis, imZeiger: true };
|
|
459
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Refs einer Quelle nachziehen, bevor eine Einschränkung gegen sie ausgewertet wird (E33). Ohne
|
|
506
|
+
* das sähe die Auswertung nur die Tags, die beim Clone schon da waren, ein später entstandener
|
|
507
|
+
* Tag bliebe unsichtbar. Einmal je Lauf und Quelle. Scheitert das Nachziehen, etwa ohne Netz,
|
|
508
|
+
* wird mit dem vorhandenen Stand weitergearbeitet.
|
|
509
|
+
*/
|
|
510
|
+
async refsNachziehen(quelle) {
|
|
511
|
+
const schluessel = quelleSchluessel(quelle);
|
|
512
|
+
if (this.nachgezogen.has(schluessel))
|
|
513
|
+
return;
|
|
514
|
+
this.nachgezogen.add(schluessel);
|
|
515
|
+
await this.p.quellen.refsNachziehen(quelle).catch(() => undefined);
|
|
516
|
+
}
|
|
460
517
|
/**
|
|
461
518
|
* Fund zu einem Skill aus dem Zeiger (E33). Die Einschränkung wird zuerst nach Composer-Regeln
|
|
462
519
|
* gegen die Tags der Quellen ausgewertet, der höchste passende Tag gewinnt. Findet sich keiner,
|
|
@@ -467,8 +524,9 @@ export class Anwendungsfaelle {
|
|
|
467
524
|
if (einschraenkung === null)
|
|
468
525
|
return await this.skillFinden(name, quelleHinweis);
|
|
469
526
|
const zustand = await this.p.zustand.lesen();
|
|
470
|
-
const quellen = zustand.quellen.filter((q) =>
|
|
527
|
+
const quellen = zustand.quellen.filter((q) => quellePasst(q, quelleHinweis));
|
|
471
528
|
for (const quelle of quellen) {
|
|
529
|
+
await this.refsNachziehen(quelle);
|
|
472
530
|
const tags = await this.p.quellen.tags(quelle).catch(() => []);
|
|
473
531
|
const ref = passenderRef(tags, einschraenkung);
|
|
474
532
|
if (ref === null)
|
|
@@ -502,7 +560,7 @@ export class Anwendungsfaelle {
|
|
|
502
560
|
if (fund.ergebnis === 'gefunden')
|
|
503
561
|
zeigerPfad = fund.pfad;
|
|
504
562
|
else if (fund.ergebnis === 'mehrdeutig')
|
|
505
|
-
|
|
563
|
+
zeigerPfad = await this.zeigerWaehlen(verzeichnis, fund.kandidaten);
|
|
506
564
|
}
|
|
507
565
|
if (zeigerPfad === undefined) {
|
|
508
566
|
ergebnis.hinweise.push(`Kein Zeiger unter ${verzeichnis}, nichts zu tun.`);
|
|
@@ -520,7 +578,7 @@ export class Anwendungsfaelle {
|
|
|
520
578
|
if (zustand.quellen.some((q) => quelleSchluessel(q) === key))
|
|
521
579
|
continue;
|
|
522
580
|
zustand.quellen.push(quelle);
|
|
523
|
-
ergebnis.gelegt.push(`Quelle ${
|
|
581
|
+
ergebnis.gelegt.push(`Quelle ${quelleAlsText(quelle)}`);
|
|
524
582
|
}
|
|
525
583
|
if (!zustand.arbeitsverzeichnisse.includes(verzeichnis))
|
|
526
584
|
zustand.arbeitsverzeichnisse.push(verzeichnis);
|
|
@@ -615,7 +673,51 @@ export class Anwendungsfaelle {
|
|
|
615
673
|
await harness.plugins.aktualisieren();
|
|
616
674
|
pluginHarnesses.push(harness.name);
|
|
617
675
|
}
|
|
618
|
-
return { checkouts, pluginHarnesses };
|
|
676
|
+
return { checkouts, pluginHarnesses, aufraeumen: await this.verwaisteAufraeumen(checkouts) };
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Pfade, in die ein Link zeigt. Gelesen wird wie beim Ist-Zustand (E19): die Ziele der
|
|
680
|
+
* vorhandenen Harnesses, die Arbeitsverzeichnisse aus dem Zustand. Ein Link, dessen Quelle nicht
|
|
681
|
+
* im Zustand steht, zeigt trotzdem in einen Checkout und hält ihn.
|
|
682
|
+
*/
|
|
683
|
+
async belegteCheckoutPfade() {
|
|
684
|
+
const pfade = new Set();
|
|
685
|
+
for (const zielZustand of await this.istZustand())
|
|
686
|
+
for (const eintrag of zielZustand.eintraege) {
|
|
687
|
+
if (eintrag.art === 'link')
|
|
688
|
+
pfade.add(path.resolve(eintrag.link.checkout.pfad));
|
|
689
|
+
else if (eintrag.art === 'link-ohne-quelle')
|
|
690
|
+
pfade.add(path.resolve(eintrag.checkoutPfad));
|
|
691
|
+
}
|
|
692
|
+
return pfade;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Verwaiste Ref-Checkouts aufräumen. Verwaist heißt: kein Link in irgendeinem Ziel zeigt hinein.
|
|
696
|
+
* Der Basis-Clone je Repo ist nie dabei, er steht in keiner Checkout-Liste (E22). Ein Checkout
|
|
697
|
+
* mit lokaler Änderung bleibt liegen und wird nur gemeldet (E15), er kommt gar nicht erst in die
|
|
698
|
+
* Frage. Gefragt wird einmal für alle Kandidaten (E28); wo niemand antwortet, gilt Nein und es
|
|
699
|
+
* wird nichts gelöscht.
|
|
700
|
+
*/
|
|
701
|
+
async verwaisteAufraeumen(checkouts) {
|
|
702
|
+
const belegt = await this.belegteCheckoutPfade();
|
|
703
|
+
const verwaiste = [];
|
|
704
|
+
for (const { checkout, aktualisierung } of checkouts) {
|
|
705
|
+
if (belegt.has(path.resolve(checkout.pfad)))
|
|
706
|
+
continue;
|
|
707
|
+
// Übersprungen wird genau der Checkout mit lokaler Änderung, ein zweites Nachfragen erübrigt sich.
|
|
708
|
+
const lokaleAenderung = aktualisierung.ergebnis === 'uebersprungen';
|
|
709
|
+
verwaiste.push({ checkout, lokaleAenderung, geloescht: false });
|
|
710
|
+
}
|
|
711
|
+
const kandidaten = verwaiste.filter((v) => !v.lokaleAenderung);
|
|
712
|
+
if (kandidaten.length === 0)
|
|
713
|
+
return { verwaiste, gefragt: false };
|
|
714
|
+
if (!(await this.p.rueckfrage.bestaetigen(aufraeumFrage(kandidaten.map((k) => k.checkout)))))
|
|
715
|
+
return { verwaiste, gefragt: true };
|
|
716
|
+
for (const kandidat of kandidaten) {
|
|
717
|
+
await this.p.quellen.entfernen(kandidat.checkout);
|
|
718
|
+
kandidat.geloescht = true;
|
|
719
|
+
}
|
|
720
|
+
return { verwaiste, gefragt: true };
|
|
619
721
|
}
|
|
620
722
|
/**
|
|
621
723
|
* Übernahme (B12, E31): fremde Symlinks eines Ziels durch eigene Links auf einen hmgr-Checkout
|
|
@@ -633,7 +735,10 @@ export class Anwendungsfaelle {
|
|
|
633
735
|
throw new OrtNichtUnterstuetzt(harness.name, args.ziel.ort.art);
|
|
634
736
|
const probelauf = args.probelauf === true;
|
|
635
737
|
const verzeichnis = harness.zielVerzeichnis(args.ziel.ort);
|
|
636
|
-
|
|
738
|
+
// Je Identität die Quelle, unter deren Schreibweise sie geführt wird (E36).
|
|
739
|
+
const bekannt = new Map();
|
|
740
|
+
for (const q of (await this.p.zustand.lesen()).quellen)
|
|
741
|
+
bekannt.set(quelleSchluessel(q), q);
|
|
637
742
|
const eintraege = [];
|
|
638
743
|
for (const eintrag of await this.p.ziele.eintraege(args.ziel, verzeichnis)) {
|
|
639
744
|
// Eigene Links sind schon fertig, sie kommen nicht in die Meldung.
|
|
@@ -661,7 +766,9 @@ export class Anwendungsfaelle {
|
|
|
661
766
|
liegenLassen('kein-branch');
|
|
662
767
|
continue;
|
|
663
768
|
}
|
|
664
|
-
|
|
769
|
+
// Kennt hmgr das Repo schon in anderer Schreibweise, zählt die geführte Fassung (E36).
|
|
770
|
+
const gefuehrt = bekannt.get(quelleSchluessel({ art: 'git', url: kopie.url }));
|
|
771
|
+
const quelle = gefuehrt ?? { art: 'git', url: kopie.url };
|
|
665
772
|
const ref = kopie.ref;
|
|
666
773
|
const skills = await this.p.quellen.skills(quelle, ref);
|
|
667
774
|
const skill = skills.find((s) => s.pfad === kopie.relativerPfad);
|
|
@@ -669,8 +776,8 @@ export class Anwendungsfaelle {
|
|
|
669
776
|
liegenLassen('skill-unbekannt');
|
|
670
777
|
continue;
|
|
671
778
|
}
|
|
672
|
-
const quelleNeu =
|
|
673
|
-
bekannt.
|
|
779
|
+
const quelleNeu = gefuehrt === undefined;
|
|
780
|
+
bekannt.set(quelleSchluessel(quelle), quelle);
|
|
674
781
|
if (!probelauf) {
|
|
675
782
|
if (quelleNeu)
|
|
676
783
|
await this.quelleHinzufuegen(quelle);
|
package/dist/kern/modell.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Begriffe aus CONTEXT.md. Nur Werte und Regeln, keine Pfade zu Dateien außerhalb des Modells.
|
|
2
|
+
import { ortSchluessel, urlOrt } from "./quelle-ort.js";
|
|
2
3
|
/** Ansprache eines Plugins im Zeiger und gegenüber der claude-CLI: `<plugin>@<marketplace>`. */
|
|
3
4
|
export function pluginSchluessel(plugin) {
|
|
4
5
|
return `${plugin.name}@${plugin.marketplace}`;
|
|
@@ -45,8 +46,22 @@ export function zielSchluessel(ziel) {
|
|
|
45
46
|
const ort = o.art === 'home' ? 'home' : o.art === 'profil' ? `profil:${o.name}` : `dir:${o.pfad}`;
|
|
46
47
|
return `${ziel.harness}/${ort}`;
|
|
47
48
|
}
|
|
49
|
+
/** Wie eine Quelle geschrieben steht: ihre URL oder ihr Pfad. Für Ausgaben, nicht für Vergleiche. */
|
|
50
|
+
export function quelleAlsText(quelle) {
|
|
51
|
+
return quelle.art === 'git' ? quelle.url : quelle.pfad;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Identität einer Quelle (E36): bei einer Git-Quelle Host, Besitzer und Repo, nicht die
|
|
55
|
+
* Schreibweise der URL. SSH- und HTTPS-Form desselben Repos sind dieselbe Quelle, mit und ohne
|
|
56
|
+
* `.git`-Suffix ebenso, und der Host zählt ohne Rücksicht auf Groß- und Kleinschreibung. Ein Text,
|
|
57
|
+
* der sich nicht als URL lesen lässt, bleibt sein eigener Schlüssel. Lokale Quellen bleiben ihr
|
|
58
|
+
* Pfad (B11).
|
|
59
|
+
*/
|
|
48
60
|
export function quelleSchluessel(quelle) {
|
|
49
|
-
|
|
61
|
+
if (quelle.art === 'lokal')
|
|
62
|
+
return `lokal:${quelle.pfad}`;
|
|
63
|
+
const ort = urlOrt(quelle.url);
|
|
64
|
+
return ort === undefined ? quelle.url : ortSchluessel(ort);
|
|
50
65
|
}
|
|
51
66
|
export class NameBelegt extends Error {
|
|
52
67
|
ziel;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Identität einer Git-Quelle (E36): Host, Besitzer und Repo, nicht die Schreibweise der URL.
|
|
2
|
+
function ohneGitSuffix(name) {
|
|
3
|
+
return name.endsWith('.git') ? name.slice(0, -4) : name;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Die letzten beiden nicht leeren Segmente sind Besitzer und Repo. Der Host wird kleingeschrieben,
|
|
7
|
+
* Hostnamen sind case-insensitiv; der Pfadanteil bleibt, wie er geschrieben steht (E36).
|
|
8
|
+
*/
|
|
9
|
+
export function ortAusSegmenten(host, segmente) {
|
|
10
|
+
const gefiltert = segmente.filter((s) => s.length > 0);
|
|
11
|
+
const repo = ohneGitSuffix(gefiltert[gefiltert.length - 1] ?? 'repo');
|
|
12
|
+
const besitzer = gefiltert[gefiltert.length - 2] ?? '_';
|
|
13
|
+
return { host: host.toLowerCase(), besitzer, repo };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Zerlegt eine Git-URL: `git@host:besitzer/repo.git`, `ssh://git@host/besitzer/repo`,
|
|
17
|
+
* `https://host/besitzer/repo.git`, `file:///pfad`. Ein Text, der keine URL ist, gibt undefined.
|
|
18
|
+
*/
|
|
19
|
+
export function urlOrt(url) {
|
|
20
|
+
const text = url.trim().replace(/\/+$/, '');
|
|
21
|
+
if (text.includes('://')) {
|
|
22
|
+
let zerlegt;
|
|
23
|
+
try {
|
|
24
|
+
zerlegt = new URL(text);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const segmente = decodeURIComponent(zerlegt.pathname).split('/');
|
|
30
|
+
// file: hat keinen Host, alle lokalen Quellen liegen unter „local".
|
|
31
|
+
if (zerlegt.protocol === 'file:')
|
|
32
|
+
return ortAusSegmenten('local', segmente);
|
|
33
|
+
return ortAusSegmenten(zerlegt.hostname, segmente);
|
|
34
|
+
}
|
|
35
|
+
const scp = /^(?:[^@/]+@)?([^@/:]+):(.+)$/.exec(text);
|
|
36
|
+
if (scp)
|
|
37
|
+
return ortAusSegmenten(scp[1], scp[2].split('/'));
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
export function ortSchluessel(ort) {
|
|
41
|
+
return `${ort.host}/${ort.besitzer}/${ort.repo}`;
|
|
42
|
+
}
|