hmgr 0.0.3 → 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/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 +16 -2
- package/dist/cli/main.js +110 -12
- 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
|
@@ -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
|
}
|
package/dist/cli/hilfe.js
CHANGED
|
@@ -33,6 +33,10 @@ und ihren Marketplace und merkt sie unter ~/.hmgr. Erst danach kennt hmgr die Sk
|
|
|
33
33
|
denen link und plugin add arbeiten. Eine URL wird geklont, ein Pfad bleibt die Arbeitskopie, die
|
|
34
34
|
dort schon liegt, und wird von hmgr nicht verändert.
|
|
35
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
|
+
|
|
36
40
|
Schalter: keine.
|
|
37
41
|
|
|
38
42
|
Beispiele:
|
|
@@ -43,6 +47,7 @@ Beispiele:
|
|
|
43
47
|
Häufige Fälle:
|
|
44
48
|
„Skill … liegt in keiner Quelle": die Quelle fehlt, erst source add, dann link.
|
|
45
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.
|
|
46
51
|
|
|
47
52
|
Siehe auch: hmgr help link, hmgr help plugin, hmgr help update`;
|
|
48
53
|
const DIR = `hmgr dir add <pfad> [--composer <pfad>]
|
|
@@ -55,7 +60,8 @@ Plugins des Vorhabens stehen. Ohne Zeiger bleibt das Verzeichnis gemerkt und jed
|
|
|
55
60
|
(E26). dir forget löst Bindung und Eintrag, die gelegten Links bleiben liegen.
|
|
56
61
|
|
|
57
62
|
Schalter:
|
|
58
|
-
--composer <pfad> composer.json wählen, wenn unter dem Verzeichnis mehrere in
|
|
63
|
+
--composer <pfad> nur bei add: composer.json wählen, wenn unter dem Verzeichnis mehrere in
|
|
64
|
+
Frage kommen
|
|
59
65
|
|
|
60
66
|
Beispiele:
|
|
61
67
|
hmgr dir add ~/Code/site
|
|
@@ -63,7 +69,8 @@ Beispiele:
|
|
|
63
69
|
hmgr dir list
|
|
64
70
|
|
|
65
71
|
Häufige Fälle:
|
|
66
|
-
„Mehrere composer.json unter …":
|
|
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.
|
|
67
74
|
link --ort dir:<pfad> merkt das Verzeichnis von selbst. dir add braucht es vorher nur, wenn die
|
|
68
75
|
composer.json mehrdeutig ist.
|
|
69
76
|
|
|
@@ -201,6 +208,11 @@ Plugin-Fähigkeit deren Plugins nach (E14). Die Links zeigen weiter auf dieselbe
|
|
|
201
208
|
nicht erneuert werden. Ein Checkout mit lokaler Änderung wird übersprungen und gemeldet (E15),
|
|
202
209
|
damit nichts verloren geht. Lokale Quellen haben keinen Checkout und bleiben unberührt.
|
|
203
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
|
+
|
|
204
216
|
So wird eine Änderung an einem Skill überall wirksam: in der Quelle committen und pushen, dann
|
|
205
217
|
hmgr update.
|
|
206
218
|
|
|
@@ -213,6 +225,8 @@ Häufige Fälle:
|
|
|
213
225
|
„übersprungen … lokale Änderung (E15)": im Checkout liegt eine Änderung, die durch einen Link
|
|
214
226
|
hindurch gemacht wurde. Dort committen und pushen, dann erneut hmgr update.
|
|
215
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.
|
|
216
230
|
|
|
217
231
|
Siehe auch: hmgr help sync, hmgr help status`;
|
|
218
232
|
const STATUS = `hmgr status
|
package/dist/cli/main.js
CHANGED
|
@@ -36,9 +36,44 @@ export function bauen(umgebung, optionen = {}) {
|
|
|
36
36
|
};
|
|
37
37
|
return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
|
|
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
|
+
};
|
|
41
75
|
function schalterLesen(args, kommando) {
|
|
76
|
+
const erlaubt = SCHALTER[kommando];
|
|
42
77
|
const rest = [];
|
|
43
78
|
const werte = new Map();
|
|
44
79
|
const flaggen = new Set();
|
|
@@ -49,18 +84,50 @@ function schalterLesen(args, kommando) {
|
|
|
49
84
|
continue;
|
|
50
85
|
}
|
|
51
86
|
const name = arg.slice(2);
|
|
52
|
-
if (
|
|
87
|
+
if (erlaubt.flaggen.includes(name)) {
|
|
53
88
|
flaggen.add(name);
|
|
54
89
|
continue;
|
|
55
90
|
}
|
|
91
|
+
if (!erlaubt.werte.includes(name))
|
|
92
|
+
throw new Error(`Unbekannter Schalter --${name} für ${kommando}. hmgr help ${kommando} zeigt die Schalter.`);
|
|
56
93
|
const wert = args[++i];
|
|
57
94
|
if (wert === undefined)
|
|
58
95
|
throw new Error(`Schalter --${name} braucht einen Wert, etwa --${name} <wert>. ` +
|
|
59
|
-
|
|
96
|
+
`hmgr help ${kommando} zeigt die Schalter.`);
|
|
60
97
|
werte.set(name, wert);
|
|
61
98
|
}
|
|
62
99
|
return { rest, werte, flaggen };
|
|
63
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
|
+
}
|
|
64
131
|
function ortLesen(text) {
|
|
65
132
|
const roh = text ?? 'home';
|
|
66
133
|
if (roh === 'home')
|
|
@@ -107,13 +174,21 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
107
174
|
return 0;
|
|
108
175
|
}
|
|
109
176
|
if (kommando === 'source') {
|
|
110
|
-
const
|
|
177
|
+
const schalter = schalterLesen(argv.slice(1), 'source');
|
|
178
|
+
unterkommandoPruefen('source', schalter);
|
|
179
|
+
const rest = schalter.rest;
|
|
180
|
+
const unterkommando = rest[0];
|
|
111
181
|
if (unterkommando === 'add') {
|
|
112
|
-
const text =
|
|
182
|
+
const text = rest[1];
|
|
113
183
|
if (text === undefined)
|
|
114
184
|
throw new Error('source add braucht eine URL oder einen Pfad, etwa hmgr source add https://example.com/skills.git');
|
|
115
|
-
const
|
|
116
|
-
const { skills, marketplace } = await anwendungsfaelle.quelleHinzufuegen(
|
|
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.`);
|
|
117
192
|
console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}), ${skills.length} Skills:`);
|
|
118
193
|
for (const skill of skills)
|
|
119
194
|
console.log(` ${skill.name} ${skill.pfad}`);
|
|
@@ -137,7 +212,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
137
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.`);
|
|
138
213
|
}
|
|
139
214
|
if (kommando === 'dir') {
|
|
140
|
-
const
|
|
215
|
+
const schalter = schalterLesen(argv.slice(1), 'dir');
|
|
216
|
+
unterkommandoPruefen('dir', schalter);
|
|
217
|
+
const { rest, werte } = schalter;
|
|
141
218
|
const unterkommando = rest[0];
|
|
142
219
|
if (unterkommando === 'add') {
|
|
143
220
|
const pfad = rest[1];
|
|
@@ -176,6 +253,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
176
253
|
}
|
|
177
254
|
if (kommando === 'link' || kommando === 'unlink') {
|
|
178
255
|
const { rest, werte, flaggen } = schalterLesen(argv.slice(1), kommando);
|
|
256
|
+
restPruefen(rest, 1, kommando);
|
|
179
257
|
const skill = rest[0];
|
|
180
258
|
if (skill === undefined)
|
|
181
259
|
throw new Error(`${kommando} braucht einen Skill-Namen, etwa hmgr ${kommando} <skill>. ` +
|
|
@@ -213,6 +291,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
213
291
|
}
|
|
214
292
|
if (kommando === 'adopt') {
|
|
215
293
|
const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'adopt');
|
|
294
|
+
restPruefen(rest, 1, 'adopt');
|
|
216
295
|
const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
|
|
217
296
|
const probelauf = flaggen.has('probelauf');
|
|
218
297
|
const name = rest[0];
|
|
@@ -251,7 +330,9 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
251
330
|
return 0;
|
|
252
331
|
}
|
|
253
332
|
if (kommando === 'plugin') {
|
|
254
|
-
const
|
|
333
|
+
const schalter = schalterLesen(argv.slice(1), 'plugin');
|
|
334
|
+
unterkommandoPruefen('plugin', schalter);
|
|
335
|
+
const { rest, werte, flaggen } = schalter;
|
|
255
336
|
const unterkommando = rest[0];
|
|
256
337
|
if (unterkommando === 'list') {
|
|
257
338
|
const marketplaces = await anwendungsfaelle.marketplaces();
|
|
@@ -301,6 +382,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
301
382
|
}
|
|
302
383
|
if (kommando === 'sync') {
|
|
303
384
|
const { rest } = schalterLesen(argv.slice(1), 'sync');
|
|
385
|
+
restPruefen(rest, 1, 'sync');
|
|
304
386
|
const ergebnis = await anwendungsfaelle.abgleich(rest[0]);
|
|
305
387
|
if (ergebnis.zeigerPfad !== undefined)
|
|
306
388
|
console.log(`Zeiger ${ergebnis.zeigerPfad}`);
|
|
@@ -317,7 +399,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
317
399
|
return 0;
|
|
318
400
|
}
|
|
319
401
|
if (kommando === 'update') {
|
|
320
|
-
|
|
402
|
+
restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
|
|
403
|
+
const { checkouts: liste, pluginHarnesses, aufraeumen } = await anwendungsfaelle.aktualisieren();
|
|
321
404
|
if (liste.length === 0)
|
|
322
405
|
console.log('Keine Checkouts.');
|
|
323
406
|
for (const { checkout, aktualisierung } of liste) {
|
|
@@ -331,9 +414,24 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
|
|
|
331
414
|
}
|
|
332
415
|
for (const name of pluginHarnesses)
|
|
333
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
|
+
}
|
|
334
431
|
return 0;
|
|
335
432
|
}
|
|
336
433
|
if (kommando === 'status') {
|
|
434
|
+
restPruefen(schalterLesen(argv.slice(1), 'status').rest, 0, 'status');
|
|
337
435
|
for (const zielZustand of await anwendungsfaelle.istZustand()) {
|
|
338
436
|
console.log(`Ziel ${zielZustand.ziel.harness} ${beschreibeOrt(zielZustand.ziel.ort)} (${zielZustand.verzeichnis})`);
|
|
339
437
|
if (zielZustand.eintraege.length === 0)
|
|
@@ -398,7 +496,7 @@ function naechsterSchritt(fehler) {
|
|
|
398
496
|
if (fehler instanceof AbhaengigkeitFehlt)
|
|
399
497
|
return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
|
|
400
498
|
if (fehler instanceof KeinPassenderRef)
|
|
401
|
-
return '
|
|
499
|
+
return 'Die Tags der Quelle sind frisch geholt, keiner passt. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
|
|
402
500
|
if (fehler instanceof HarnessNichtVorhanden)
|
|
403
501
|
return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
|
|
404
502
|
if (fehler instanceof OrtNichtUnterstuetzt)
|
|
@@ -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
|
+
}
|