hmgr 0.0.3 → 0.0.5

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/cli/main.js CHANGED
@@ -5,7 +5,7 @@ import { realpathSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { Anwendungsfaelle } from "../kern/anwendungsfaelle.js";
8
- import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, SkillMehrdeutig, SkillUnbekannt, ZeigerMehrdeutig, pluginSchluessel, } from "../kern/modell.js";
8
+ import { AbhaengigkeitFehlt, EinschraenkungOhneZeiger, HarnessNichtVorhanden, HarnessTypUnbekannt, KeinPassenderRef, NameBelegt, OrtNichtUnterstuetzt, PluginMehrdeutig, PluginUnbekannt, QuelleAufraeumenLokaleAenderungen, QuelleNameBelegt, QuelleUnbekannt, SkillMehrdeutig, SkillOrdnerBereitsVerwaltet, SkillOrdnerFaehigkeitFehlt, SkillOrdnerNichtVerwaltet, SkillOrdnerUngueltig, SkillUnbekannt, ZeigerMehrdeutig, ZeigerSchreibenGesperrt, pluginSchluessel, quelleName, quelleSchluessel, } from "../kern/modell.js";
9
9
  import { Agents } from "../adapter/agents.js";
10
10
  import { ClaudeCode } from "../adapter/claude-code.js";
11
11
  import { Hermes } from "../adapter/hermes.js";
@@ -18,7 +18,12 @@ import { ZustandJson } from "../adapter/zustand-json.js";
18
18
  import { HILFE, hilfeText } from "./hilfe.js";
19
19
  export function umgebungLesen(env = process.env) {
20
20
  const home = env.HOME ?? homedir();
21
- return { home, hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr') };
21
+ const xdgStateHome = env.XDG_STATE_HOME;
22
+ return {
23
+ home,
24
+ hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr'),
25
+ ...(xdgStateHome !== undefined && xdgStateHome.length > 0 ? { xdgStateHome } : {}),
26
+ };
22
27
  }
23
28
  export function bauen(umgebung, optionen = {}) {
24
29
  const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'));
@@ -29,16 +34,62 @@ export function bauen(umgebung, optionen = {}) {
29
34
  const ports = {
30
35
  harnesses: [claude, new Hermes(umgebung.home), new Agents(umgebung.home)],
31
36
  quellen: new GitQuellen(wurzel),
32
- ziele: new DateisystemZiele(wurzel, zustand),
37
+ ziele: new DateisystemZiele(wurzel, zustand, umgebung.xdgStateHome),
33
38
  zeiger: new ComposerZeiger(),
34
39
  zustand,
35
40
  rueckfrage: optionen.rueckfrage ?? new TerminalRueckfrage(),
36
41
  };
37
42
  return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
38
43
  }
39
- /** Schalter ohne Wert. Alles andere braucht einen. `help` steht hier, damit --help nie greift. */
40
- const FLAGGEN = new Set(['ersetzen', 'local', 'lokal', 'probelauf', 'help']);
44
+ /**
45
+ * Was jedes Kommando auswertet, gleichlautend mit seiner Hilfe. Alles andere bricht ab. `--help`
46
+ * steht nirgends, weil der Hilfe-Dispatch vor jedem schalterLesen läuft.
47
+ */
48
+ const SCHALTER = {
49
+ source: { werte: ['name'], flaggen: [] },
50
+ dir: { werte: ['composer'], flaggen: [] },
51
+ link: {
52
+ werte: ['harness', 'ort', 'composer', 'quelle', 'version'],
53
+ flaggen: ['local', 'lokal', 'ersetzen'],
54
+ },
55
+ unlink: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal'] },
56
+ adopt: { werte: ['harness', 'ort', 'composer'], flaggen: ['local', 'lokal', 'probelauf'] },
57
+ plugin: { werte: ['harness', 'ort', 'composer', 'quelle'], flaggen: ['local', 'lokal'] },
58
+ sync: { werte: [], flaggen: [] },
59
+ update: { werte: [], flaggen: [] },
60
+ status: { werte: [], flaggen: [] },
61
+ harness: { werte: ['profil'], flaggen: [] },
62
+ };
63
+ /**
64
+ * Was ein einzelnes Unterkommando von seiner Familie liest, gleichlautend mit ihrer Hilfe. Ohne
65
+ * `schalter` bleibt es bei der Familienliste, das reicht, solange alle Unterkommandos dieselben
66
+ * Schalter lesen.
67
+ */
68
+ const UNTERKOMMANDOS = {
69
+ source: {
70
+ add: { woerter: 2, schalter: ['name'] },
71
+ remove: { woerter: 2, schalter: [] },
72
+ rename: { woerter: 3, schalter: [] },
73
+ list: { woerter: 1, schalter: [] },
74
+ },
75
+ dir: {
76
+ add: { woerter: 2, schalter: ['composer'] },
77
+ forget: { woerter: 2, schalter: [] },
78
+ list: { woerter: 1, schalter: [] },
79
+ },
80
+ plugin: {
81
+ add: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'quelle', 'local', 'lokal'] },
82
+ remove: { woerter: 2, schalter: ['harness', 'ort', 'composer', 'local', 'lokal'] },
83
+ list: { woerter: 1, schalter: [] },
84
+ },
85
+ harness: {
86
+ list: { woerter: 1, schalter: [] },
87
+ enable: { woerter: 2, schalter: [] },
88
+ disable: { woerter: 2, schalter: [] },
89
+ },
90
+ };
41
91
  function schalterLesen(args, kommando) {
92
+ const erlaubt = SCHALTER[kommando];
42
93
  const rest = [];
43
94
  const werte = new Map();
44
95
  const flaggen = new Set();
@@ -49,18 +100,50 @@ function schalterLesen(args, kommando) {
49
100
  continue;
50
101
  }
51
102
  const name = arg.slice(2);
52
- if (FLAGGEN.has(name)) {
103
+ if (erlaubt.flaggen.includes(name)) {
53
104
  flaggen.add(name);
54
105
  continue;
55
106
  }
107
+ if (!erlaubt.werte.includes(name))
108
+ throw new Error(`Unbekannter Schalter --${name} für ${kommando}. hmgr help ${kommando} zeigt die Schalter.`);
56
109
  const wert = args[++i];
57
110
  if (wert === undefined)
58
111
  throw new Error(`Schalter --${name} braucht einen Wert, etwa --${name} <wert>. ` +
59
- `${kommando === undefined ? 'hmgr --help' : `hmgr help ${kommando}`} zeigt die Schalter.`);
112
+ `hmgr help ${kommando} zeigt die Schalter.`);
60
113
  werte.set(name, wert);
61
114
  }
62
115
  return { rest, werte, flaggen };
63
116
  }
117
+ /** Ein Wort mehr, als das Kommando liest, ist ein Tippfehler und kein stiller Zusatz. */
118
+ function restPruefen(rest, woerter, kommando, unterkommando) {
119
+ const zuviel = rest[woerter];
120
+ if (zuviel === undefined)
121
+ return;
122
+ const verwendung = unterkommando === undefined ? kommando : `${kommando} ${unterkommando}`;
123
+ // Der Name des Unterkommandos zählt zu den Wörtern, aber nicht zu den Argumenten.
124
+ const argumente = woerter - (unterkommando === undefined ? 0 : 1);
125
+ throw new Error(`${verwendung} erwartet ${argumente === 0 ? 'kein' : 'kein weiteres'} Argument „${zuviel}". ` +
126
+ `hmgr help ${kommando} zeigt die Verwendung.`);
127
+ }
128
+ /**
129
+ * Prüft, was erst mit dem Unterkommando feststeht: liest es die gesetzten Schalter, und steht ein
130
+ * Wort zu viel da. Ein unbekanntes Unterkommando meldet die Familie selbst.
131
+ */
132
+ function unterkommandoPruefen(kommando, schalter) {
133
+ const unterkommando = schalter.rest[0];
134
+ if (unterkommando === undefined)
135
+ return;
136
+ const menge = UNTERKOMMANDOS[kommando][unterkommando];
137
+ if (menge === undefined)
138
+ return;
139
+ const erlaubt = menge.schalter;
140
+ if (erlaubt !== undefined)
141
+ for (const name of [...schalter.werte.keys(), ...schalter.flaggen])
142
+ if (!erlaubt.includes(name))
143
+ throw new Error(`Unbekannter Schalter --${name} für ${kommando} ${unterkommando}. ` +
144
+ `hmgr help ${kommando} zeigt die Schalter.`);
145
+ restPruefen(schalter.rest, menge.woerter, kommando, unterkommando);
146
+ }
64
147
  function ortLesen(text) {
65
148
  const roh = text ?? 'home';
66
149
  if (roh === 'home')
@@ -78,6 +161,8 @@ const UEBERNAHME_GRUND = {
78
161
  'keine-url': 'Arbeitskopie ohne remote.origin.url',
79
162
  'kein-branch': 'Arbeitskopie ohne Branch (detached HEAD)',
80
163
  'skill-unbekannt': 'Pfad ist in der Quelle kein Skill',
164
+ 'lokale-aenderungen': 'Ordner weicht von der Quelle ab',
165
+ 'nicht-pruefbar': 'hmgr kann nicht prüfen, ob der Ordner der Quelle entspricht',
81
166
  };
82
167
  export async function ausfuehren(argv, umgebung, optionen = {}) {
83
168
  const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
@@ -107,16 +192,26 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
107
192
  return 0;
108
193
  }
109
194
  if (kommando === 'source') {
110
- const unterkommando = argv[1];
195
+ const schalter = schalterLesen(argv.slice(1), 'source');
196
+ unterkommandoPruefen('source', schalter);
197
+ const { rest, werte } = schalter;
198
+ const unterkommando = rest[0];
111
199
  if (unterkommando === 'add') {
112
- const text = argv[2];
200
+ const text = rest[1];
113
201
  if (text === undefined)
114
202
  throw new Error('source add braucht eine URL oder einen Pfad, etwa hmgr source add https://example.com/skills.git');
115
- const quelle = quelleAusText(text);
116
- const { skills, marketplace } = await anwendungsfaelle.quelleHinzufuegen(quelle);
117
- console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}), ${skills.length} Skills:`);
118
- for (const skill of skills)
119
- console.log(` ${skill.name} ${skill.pfad}`);
203
+ const genannt = quelleAusText(text);
204
+ const { quelle, skills, marketplace, bekannt } = await anwendungsfaelle.quelleHinzufuegen(genannt, werte.get('name'));
205
+ // Dieselbe Quelle in anderer Schreibweise legt keine zweite an (E36).
206
+ if (bekannt)
207
+ console.log(quelleAlsText(quelle) === quelleAlsText(genannt)
208
+ ? 'Quelle ist schon bekannt, keine zweite angelegt.'
209
+ : `Quelle ist schon bekannt, geführt als ${quelleAlsText(quelle)}. Keine zweite angelegt.`);
210
+ console.log(`Quelle ${quelleAlsText(quelle)} (${quelle.art}) als „${quelleName(quelle)}", ${skills.length} Skills:`);
211
+ for (const skill of skills) {
212
+ const beschreibung = skill.beschreibung === undefined ? '' : ` ${kappen(skill.beschreibung)}`;
213
+ console.log(` ${skill.name} ${skill.pfad}${beschreibung}`);
214
+ }
120
215
  if (marketplace !== null) {
121
216
  console.log(`Marketplace ${marketplace.name}, ${marketplace.plugins.length} Plugins:`);
122
217
  for (const plugin of marketplace.plugins) {
@@ -126,18 +221,94 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
126
221
  }
127
222
  return 0;
128
223
  }
224
+ if (unterkommando === 'remove') {
225
+ const text = rest[1];
226
+ if (text === undefined)
227
+ throw new Error('source remove braucht eine URL oder einen Pfad. hmgr source list zeigt die bekannten Quellen.');
228
+ const genannt = quelleAusText(text);
229
+ // Inventar (E58) vor dem Vergessen erheben (F14): danach tragen die Links ihre Quelle nicht
230
+ // mehr. Bei unbekannter Quelle entfällt der Ziel-Scan, den Fehler wirft quelleEntfernen.
231
+ const key = quelleSchluessel(genannt);
232
+ // Die Rückfrage kommt jetzt VOR dem Vergessen (F31a), nennen muss sie die Quelle trotzdem
233
+ // unter ihrer geführten Schreibweise (E36), nicht unter der getippten.
234
+ const gefuehrtVorher = (await anwendungsfaelle.quellen()).find((q) => quelleSchluessel(q) === key);
235
+ const inventar = gefuehrtVorher === undefined ? undefined : await anwendungsfaelle.quelleAufraeumInventar(genannt);
236
+ const meldeVergessen = (gefuehrt, zahl) => {
237
+ console.log(`Quelle ${quelleAlsText(gefuehrt)} vergessen, Checkouts und Links bleiben liegen.`);
238
+ if (zahl === 0)
239
+ console.log(' Kein Link betroffen.');
240
+ else {
241
+ console.log(` ${zahl === 1 ? '1 Link gilt' : `${zahl} Links gelten`} jetzt als „Link ohne bekannte Quelle", das Aktualisieren prüft ${zahl === 1 ? 'ihn' : 'sie'} nicht mehr.`);
242
+ console.log(` source add derselben Quelle macht ${zahl === 1 ? 'ihn wieder zu einem normalen Link' : 'sie wieder zu normalen Links'}.`);
243
+ }
244
+ };
245
+ // Leeres Inventar (F31a): nichts zum Aufräumen, keine Rückfrage, sofort vergessen wie bisher.
246
+ if (gefuehrtVorher === undefined ||
247
+ inventar === undefined ||
248
+ (inventar.links.length === 0 && inventar.checkouts.length === 0)) {
249
+ const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
250
+ meldeVergessen(gefuehrt, inventar?.links.length ?? 0);
251
+ return 0;
252
+ }
253
+ // Aufräum-Rückfrage VOR dem Vergessen (F31a): ein Ja mit lokalen Änderungen darf weder löschen
254
+ // noch die Quelle vergessen, das entscheidet erst quelleAufraeumen unten.
255
+ const teile = [
256
+ zahlwort(inventar.links.length, 'Link', 'Links'),
257
+ zahlwort(inventar.checkouts.length, 'Checkout', 'Checkouts'),
258
+ ];
259
+ const bleibt = (n) => (n === 1 ? ', bleibt liegen' : ', bleiben liegen');
260
+ if (inventar.plugins.length > 0)
261
+ teile.push(zahlwort(inventar.plugins.length, 'installiertes Plugin', 'installierte Plugins') + bleibt(inventar.plugins.length));
262
+ if (inventar.zeigerPfade.length > 0)
263
+ teile.push(zahlwort(inventar.zeigerPfade.length, 'Zeiger-Eintrag', 'Zeiger-Einträge') + bleibt(inventar.zeigerPfade.length));
264
+ let frage = `Skills von ${quelleAlsText(gefuehrtVorher)} komplett vom Rechner löschen (${teile.join(', ')})?`;
265
+ if (inventar.checkoutsMitLokalenAenderungen.length > 0) {
266
+ const anzahl = inventar.checkoutsMitLokalenAenderungen.length;
267
+ frage +=
268
+ `\n${anzahl === 1 ? 'Achtung: 1 Checkout hat' : `Achtung: ${anzahl} Checkouts haben`} lokale Änderungen. ` +
269
+ 'Ein Ja bricht dann ab, ohne zu löschen, die Quelle bleibt bekannt. Erst committen/pushen oder verwerfen, dann erneut versuchen.';
270
+ }
271
+ if (!(await ports.rueckfrage.bestaetigen(frage))) {
272
+ const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
273
+ meldeVergessen(gefuehrt, inventar.links.length);
274
+ return 0;
275
+ }
276
+ // Ja: erst aufräumen (wirft bei lokalen Änderungen, VOR jeder Löschung, F31a), erst danach die
277
+ // Quelle vergessen, damit ein Abbruch sie unberührt lässt.
278
+ const ergebnis = await anwendungsfaelle.quelleAufraeumen(inventar);
279
+ const gefuehrt = await anwendungsfaelle.quelleEntfernen(genannt);
280
+ console.log(`Quelle ${quelleAlsText(gefuehrt)} entfernt und aufgeräumt.`);
281
+ console.log(` entfernt ${zahlwort(ergebnis.entfernteLinks, 'Link', 'Links')}, ${zahlwort(ergebnis.entfernteCheckouts, 'Checkout', 'Checkouts')}.`);
282
+ if (inventar.plugins.length > 0)
283
+ console.log(` installierte Plugins bleiben liegen: ${inventar.plugins.join(', ')}`);
284
+ if (inventar.zeigerPfade.length > 0)
285
+ console.log(` Zeiger-Einträge bleiben liegen: ${inventar.zeigerPfade.join(', ')}`);
286
+ return 0;
287
+ }
288
+ if (unterkommando === 'rename') {
289
+ const text = rest[1];
290
+ const name = rest[2];
291
+ if (text === undefined || name === undefined || name.trim().length === 0)
292
+ throw new Error('source rename braucht eine URL oder einen Pfad und einen neuen Namen, etwa hmgr source rename https://example.com/skills.git skills');
293
+ const genannt = quelleAusText(text);
294
+ const umbenannt = await anwendungsfaelle.quelleUmbenennen(genannt, name);
295
+ console.log(`Quelle ${quelleAlsText(umbenannt)} heißt jetzt „${quelleName(umbenannt)}".`);
296
+ return 0;
297
+ }
129
298
  if (unterkommando === 'list') {
130
299
  const zustand = await ports.zustand.lesen();
131
300
  if (zustand.quellen.length === 0)
132
301
  console.log('Keine Quellen.');
133
302
  for (const quelle of zustand.quellen)
134
- console.log(`${quelle.art} ${quelleAlsText(quelle)}`);
303
+ console.log(`${quelle.art} ${quelleName(quelle)} ${quelleAlsText(quelle)}`);
135
304
  return 0;
136
305
  }
137
- throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
306
+ throw new Error(`${unterkommando === undefined ? 'source braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für source`}: source add <url|pfad>, source rename <url|pfad> <name>, source remove <url|pfad> oder source list. hmgr help source zeigt Beispiele.`);
138
307
  }
139
308
  if (kommando === 'dir') {
140
- const { rest, werte } = schalterLesen(argv.slice(1), 'dir');
309
+ const schalter = schalterLesen(argv.slice(1), 'dir');
310
+ unterkommandoPruefen('dir', schalter);
311
+ const { rest, werte } = schalter;
141
312
  const unterkommando = rest[0];
142
313
  if (unterkommando === 'add') {
143
314
  const pfad = rest[1];
@@ -176,6 +347,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
176
347
  }
177
348
  if (kommando === 'link' || kommando === 'unlink') {
178
349
  const { rest, werte, flaggen } = schalterLesen(argv.slice(1), kommando);
350
+ restPruefen(rest, 1, kommando);
179
351
  const skill = rest[0];
180
352
  if (skill === undefined)
181
353
  throw new Error(`${kommando} braucht einen Skill-Namen, etwa hmgr ${kommando} <skill>. ` +
@@ -191,8 +363,10 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
191
363
  await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
192
364
  }
193
365
  if (kommando === 'unlink') {
194
- await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
366
+ const ergebnis = await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
195
367
  console.log(`Link ${skill} entfernt.`);
368
+ if (ergebnis.hinweis !== undefined)
369
+ console.log(ergebnis.hinweis);
196
370
  return 0;
197
371
  }
198
372
  const quelle = werte.get('quelle');
@@ -213,6 +387,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
213
387
  }
214
388
  if (kommando === 'adopt') {
215
389
  const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'adopt');
390
+ restPruefen(rest, 1, 'adopt');
216
391
  const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
217
392
  const probelauf = flaggen.has('probelauf');
218
393
  const name = rest[0];
@@ -248,10 +423,14 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
248
423
  }
249
424
  const quellenText = neueQuellen === 1 ? '1 neue Quelle' : `${neueQuellen} neue Quellen`;
250
425
  console.log(`${uebernommen} übernommen, ${uebersprungen} übersprungen, ${quellenText}.`);
426
+ if (ergebnis.hinweis !== undefined)
427
+ console.log(ergebnis.hinweis);
251
428
  return 0;
252
429
  }
253
430
  if (kommando === 'plugin') {
254
- const { rest, werte, flaggen } = schalterLesen(argv.slice(1), 'plugin');
431
+ const schalter = schalterLesen(argv.slice(1), 'plugin');
432
+ unterkommandoPruefen('plugin', schalter);
433
+ const { rest, werte, flaggen } = schalter;
255
434
  const unterkommando = rest[0];
256
435
  if (unterkommando === 'list') {
257
436
  const marketplaces = await anwendungsfaelle.marketplaces();
@@ -301,6 +480,7 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
301
480
  }
302
481
  if (kommando === 'sync') {
303
482
  const { rest } = schalterLesen(argv.slice(1), 'sync');
483
+ restPruefen(rest, 1, 'sync');
304
484
  const ergebnis = await anwendungsfaelle.abgleich(rest[0]);
305
485
  if (ergebnis.zeigerPfad !== undefined)
306
486
  console.log(`Zeiger ${ergebnis.zeigerPfad}`);
@@ -317,7 +497,8 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
317
497
  return 0;
318
498
  }
319
499
  if (kommando === 'update') {
320
- const { checkouts: liste, pluginHarnesses } = await anwendungsfaelle.aktualisieren();
500
+ restPruefen(schalterLesen(argv.slice(1), 'update').rest, 0, 'update');
501
+ const { checkouts: liste, pluginHarnesses, aufraeumen } = await anwendungsfaelle.aktualisieren();
321
502
  if (liste.length === 0)
322
503
  console.log('Keine Checkouts.');
323
504
  for (const { checkout, aktualisierung } of liste) {
@@ -326,14 +507,31 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
326
507
  console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
327
508
  else if (aktualisierung.ergebnis === 'unveraendert')
328
509
  console.log(` unverändert ${kopf}`);
329
- else
510
+ else if (aktualisierung.ergebnis === 'uebersprungen')
330
511
  console.log(` übersprungen ${kopf} lokale Änderung (E15)`);
512
+ else
513
+ console.log(` fehlgeschlagen ${kopf} ${aktualisierung.fehler}`);
331
514
  }
332
515
  for (const name of pluginHarnesses)
333
516
  console.log(` Plugins ${name} aktualisiert (E14)`);
517
+ // Zum Schluss die verwaisten Checkouts: was gelöscht wurde und was liegen bleibt.
518
+ for (const verwaist of aufraeumen.verwaiste) {
519
+ const kopf = `${quelleAlsText(verwaist.checkout.quelle)} @ ${verwaist.checkout.ref}`;
520
+ if (verwaist.geloescht)
521
+ console.log(` gelöscht ${kopf} verwaist (${verwaist.checkout.pfad})`);
522
+ else if (verwaist.lokaleAenderung)
523
+ console.log(` liegen lassen ${kopf} verwaist, aber lokale Änderung (E15)`);
524
+ else
525
+ console.log(` liegen lassen ${kopf} verwaist, ohne Bestätigung nicht gelöscht`);
526
+ }
527
+ if (aufraeumen.verwaiste.length > 0) {
528
+ const geloescht = aufraeumen.verwaiste.filter((v) => v.geloescht).length;
529
+ console.log(`${aufraeumen.verwaiste.length} verwaist, ${geloescht} gelöscht, ${aufraeumen.verwaiste.length - geloescht} liegen geblieben.`);
530
+ }
334
531
  return 0;
335
532
  }
336
533
  if (kommando === 'status') {
534
+ restPruefen(schalterLesen(argv.slice(1), 'status').rest, 0, 'status');
337
535
  for (const zielZustand of await anwendungsfaelle.istZustand()) {
338
536
  console.log(`Ziel ${zielZustand.ziel.harness} ${beschreibeOrt(zielZustand.ziel.ort)} (${zielZustand.verzeichnis})`);
339
537
  if (zielZustand.eintraege.length === 0)
@@ -342,12 +540,12 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
342
540
  if (eintrag.art === 'fremd')
343
541
  console.log(` ${eintrag.name} fremd`);
344
542
  else if (eintrag.art === 'link-ohne-quelle')
345
- console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})`);
543
+ console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.name))}`);
346
544
  else {
347
545
  // Steht im Zeiger eine Versionseinschränkung, kommt sie vor den aufgelösten Ref (E33).
348
546
  const einschraenkung = zielZustand.einschraenkungen.get(eintrag.link.name);
349
547
  const spanne = einschraenkung === undefined ? '' : `${einschraenkung} → `;
350
- console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})`);
548
+ console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})${beschreibeGrund(zielZustand.gruende.get(eintrag.link.name))}`);
351
549
  }
352
550
  }
353
551
  if (zielZustand.plugins !== undefined)
@@ -358,8 +556,132 @@ export async function ausfuehren(argv, umgebung, optionen = {}) {
358
556
  console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
359
557
  return 0;
360
558
  }
559
+ if (kommando === 'harness') {
560
+ const schalter = schalterLesen(argv.slice(1), 'harness');
561
+ unterkommandoPruefen('harness', schalter);
562
+ const { rest, werte } = schalter;
563
+ const unterkommando = rest[0];
564
+ if (unterkommando === 'skills-dir') {
565
+ const harnessName = rest[1];
566
+ if (harnessName === undefined)
567
+ throw new Error('harness skills-dir braucht einen Harness-Namen, etwa hmgr harness skills-dir hermes. ' +
568
+ 'hmgr harness list zeigt die bekannten Namen.');
569
+ const unter = rest[2];
570
+ const profilName = werte.get('profil');
571
+ const ort = profilName === undefined ? { art: 'home' } : { art: 'profil', name: profilName };
572
+ const ziel = { harness: harnessName, ort };
573
+ const ortText = beschreibeOrt(ort);
574
+ if (unter === undefined) {
575
+ restPruefen(rest, 2, 'harness', 'skills-dir');
576
+ const pfad = await anwendungsfaelle.verwalteterSkillOrdner(ziel);
577
+ if (pfad === undefined) {
578
+ console.log(`${harnessName} ${ortText}: kein Skill-Ordner verwaltet.`);
579
+ console.log(` Festlegen mit hmgr harness skills-dir ${harnessName} add <pfad>.`);
580
+ }
581
+ else {
582
+ console.log(`${harnessName} ${ortText} ${pfad}`);
583
+ }
584
+ return 0;
585
+ }
586
+ if (unter === 'add') {
587
+ restPruefen(rest, 4, 'harness', 'skills-dir');
588
+ const pfad = rest[3];
589
+ if (pfad === undefined)
590
+ throw new Error('harness skills-dir <name> add braucht einen Pfad, etwa ' +
591
+ 'hmgr harness skills-dir hermes add ~/.hermes/external-skills.');
592
+ const gesetzt = await anwendungsfaelle.skillOrdnerFestlegen(ziel, pfad);
593
+ console.log(`${harnessName} ${ortText} liest Skills jetzt aus ${gesetzt}.`);
594
+ return 0;
595
+ }
596
+ if (unter === 'remove') {
597
+ restPruefen(rest, 3, 'harness', 'skills-dir');
598
+ const ergebnis = await anwendungsfaelle.skillOrdnerEntfernen(ziel);
599
+ console.log(`${harnessName} ${ortText}: Eintrag für ${ergebnis.pfad} entfernt.`);
600
+ const inhalt = `${zahlwort(ergebnis.hmgrLinks, 'hmgr-Link', 'hmgr-Links')}, ${zahlwort(ergebnis.fremdeEintraege, 'fremder Eintrag', 'fremde Einträge')}`;
601
+ const frage = `Ordner ${ergebnis.pfad} enthält noch ${inhalt}. Löschen?`;
602
+ if (await ports.rueckfrage.bestaetigen(frage)) {
603
+ await anwendungsfaelle.skillOrdnerLoeschen(ziel, ergebnis.pfad);
604
+ console.log(` Ordner ${ergebnis.pfad} gelöscht.`);
605
+ }
606
+ return 0;
607
+ }
608
+ throw new Error(`Unbekanntes Unterkommando „${unter}" für harness skills-dir: harness skills-dir <name>, ` +
609
+ 'harness skills-dir <name> add <pfad> oder harness skills-dir <name> remove. ' +
610
+ 'hmgr help harness zeigt Beispiele.');
611
+ }
612
+ if (unterkommando === 'list') {
613
+ const aktive = new Set(await anwendungsfaelle.aktiveHarnesses());
614
+ const fehlend = new Set(await anwendungsfaelle.nichtVorhandeneHarnesses());
615
+ for (const name of anwendungsfaelle.harnessTypen()) {
616
+ const aktivText = aktive.has(name) ? 'aktiv' : 'inaktiv';
617
+ const vorhandenText = fehlend.has(name) ? 'nicht vorhanden' : 'vorhanden';
618
+ console.log(`${name} ${aktivText}, ${vorhandenText}`);
619
+ }
620
+ return 0;
621
+ }
622
+ if (unterkommando === 'enable' || unterkommando === 'disable') {
623
+ const name = rest[1];
624
+ if (name === undefined)
625
+ throw new Error(`harness ${unterkommando} braucht einen Harness-Namen. hmgr harness list zeigt die bekannten Harness-Typen.`);
626
+ const vorherAktiv = new Set(await anwendungsfaelle.aktiveHarnesses());
627
+ if (unterkommando === 'enable') {
628
+ await anwendungsfaelle.harnessAktivieren(name);
629
+ console.log(vorherAktiv.has(name) ? `${name} ist schon aktiv.` : `${name} aktiviert.`);
630
+ return 0;
631
+ }
632
+ await anwendungsfaelle.harnessDeaktivieren(name);
633
+ console.log(vorherAktiv.has(name) ? `${name} deaktiviert.` : `${name} ist schon inaktiv.`);
634
+ // Säubern-Angebot nur, wenn eigene Konfiguration liegt, mit derselben Bedingung wie die GUI
635
+ // (E38, GRILL.md Entwurfskritik 4): eigene Links oder eine liegen gebliebene Registrierung
636
+ // ohne Link (E38 R7), sonst machten CLI und GUI verschiedene Angebote.
637
+ const ziele = (await anwendungsfaelle.istZustand()).filter((z) => z.ziel.harness === name &&
638
+ (z.eintraege.some((eintrag) => eintrag.art !== 'fremd') || z.istRegistriert));
639
+ if (ziele.length === 0)
640
+ return 0;
641
+ const links = ziele.reduce((n, z) => n + z.eintraege.filter((eintrag) => eintrag.art !== 'fremd').length, 0);
642
+ const eintraege = ziele.filter((z) => z.istRegistriert).length;
643
+ // Wer den harness-eigenen Eintrag angelegt hat, weiß niemand (E38 R7), also wird er benannt,
644
+ // nicht hmgr zugeschrieben. Gleicher Text wie bilanzText in der GUI.
645
+ const teile = [
646
+ links === 0
647
+ ? zahlwort(ziele.length, 'leeres Ziel', 'leere Ziele')
648
+ : `${zahlwort(links, 'Link', 'Links')} in ${zahlwort(ziele.length, 'Ziel', 'Zielen')}`,
649
+ ];
650
+ if (eintraege > 0)
651
+ teile.push(zahlwort(eintraege, 'harness-eigener Eintrag', 'harness-eigene Einträge'));
652
+ const liste = ziele.map((z) => ` ${z.verzeichnis}`).join('\n');
653
+ const frage = `Es liegt noch Konfiguration (${teile.join(', ')}):\n${liste}\nEntfernen?`;
654
+ if (!(await ports.rueckfrage.bestaetigen(frage))) {
655
+ console.log(' Konfiguration bleibt liegen, hmgr pflegt sie nicht mehr.');
656
+ return 0;
657
+ }
658
+ const ergebnis = await anwendungsfaelle.saeubern(name);
659
+ for (const zielErgebnis of ergebnis.ziele) {
660
+ if (zielErgebnis.entfernteLinks.length > 0)
661
+ console.log(` entfernt ${beschreibeOrt(zielErgebnis.ziel.ort)} ${zielErgebnis.entfernteLinks.join(', ')}`);
662
+ if (zielErgebnis.konfigurationZurueckgebaut)
663
+ console.log(` zurückgebaut ${beschreibeOrt(zielErgebnis.ziel.ort)} harness-eigene Konfiguration`);
664
+ }
665
+ return 0;
666
+ }
667
+ throw new Error(`${unterkommando === undefined ? 'harness braucht ein Unterkommando' : `Unbekanntes Unterkommando „${unterkommando}" für harness`}: harness list, harness enable <name> oder harness disable <name>. hmgr help harness zeigt Beispiele.`);
668
+ }
361
669
  throw new Error(`Unbekanntes Kommando „${kommando}". hmgr --help zeigt alle Kommandos.`);
362
670
  }
671
+ /** Grund eines Links als Suffix (E37). „manuell" bleibt stumm, damit die Ausgabe ruhig bleibt. */
672
+ function beschreibeGrund(grund) {
673
+ if (grund === undefined || grund.art === 'manuell')
674
+ return '';
675
+ return grund.art === 'plugin' ? ` Plugin „${grund.plugin}"` : ' über Zeiger';
676
+ }
677
+ /** Zahl mit passender Endung, Singular oder Plural, für die Säubern-Frage bei harness disable (E38). */
678
+ function zahlwort(n, ein, viele) {
679
+ return `${n} ${n === 1 ? ein : viele}`;
680
+ }
681
+ /** Text auf `laenge` Zeichen kappen, mit „…" markiert, damit eine Skill-Liste lesbar bleibt (E39). */
682
+ function kappen(text, laenge = 80) {
683
+ return text.length <= laenge ? text : `${text.slice(0, laenge - 1)}…`;
684
+ }
363
685
  function beschreibeOrt(ort) {
364
686
  if (ort.art === 'home')
365
687
  return 'home';
@@ -373,12 +695,21 @@ function istModellFehler(fehler) {
373
695
  fehler instanceof SkillUnbekannt ||
374
696
  fehler instanceof OrtNichtUnterstuetzt ||
375
697
  fehler instanceof ZeigerMehrdeutig ||
698
+ fehler instanceof ZeigerSchreibenGesperrt ||
376
699
  fehler instanceof HarnessNichtVorhanden ||
700
+ fehler instanceof HarnessTypUnbekannt ||
377
701
  fehler instanceof KeinPassenderRef ||
378
702
  fehler instanceof EinschraenkungOhneZeiger ||
379
703
  fehler instanceof PluginMehrdeutig ||
380
704
  fehler instanceof PluginUnbekannt ||
381
- fehler instanceof AbhaengigkeitFehlt);
705
+ fehler instanceof AbhaengigkeitFehlt ||
706
+ fehler instanceof QuelleUnbekannt ||
707
+ fehler instanceof QuelleNameBelegt ||
708
+ fehler instanceof QuelleAufraeumenLokaleAenderungen ||
709
+ fehler instanceof SkillOrdnerFaehigkeitFehlt ||
710
+ fehler instanceof SkillOrdnerBereitsVerwaltet ||
711
+ fehler instanceof SkillOrdnerNichtVerwaltet ||
712
+ fehler instanceof SkillOrdnerUngueltig);
382
713
  }
383
714
  /**
384
715
  * Der nächste Schritt zu einem Fehler aus dem Kern. Die Fehlertexte selbst bleiben, wie das Modell
@@ -398,14 +729,29 @@ function naechsterSchritt(fehler) {
398
729
  if (fehler instanceof AbhaengigkeitFehlt)
399
730
  return 'Die Abhängigkeit steht in keinem Plugin desselben Marketplace, das kann nur die Quelle beheben.';
400
731
  if (fehler instanceof KeinPassenderRef)
401
- return 'hmgr update holt neue Tags der Quelle. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
732
+ return 'Die Tags der Quelle sind frisch geholt, keiner passt. Sonst einen vorhandenen Tag- oder Branchnamen als --version nennen.';
402
733
  if (fehler instanceof HarnessNichtVorhanden)
403
734
  return 'Der Harness legt sein Verzeichnis beim ersten Start an. hmgr status zeigt, welche Harnesses hier liegen.';
404
735
  if (fehler instanceof OrtNichtUnterstuetzt)
405
736
  return 'profil:<name> kennt nur hermes, claude-code und agents kennen home und dir:<pfad>.';
406
- const text = fehler instanceof Error ? fehler.message : '';
407
- if (text.startsWith('Unbekannter Harness'))
408
- return 'Bekannt sind claude-code, hermes und agents.';
737
+ if (fehler instanceof HarnessTypUnbekannt)
738
+ return 'Bekannt sind claude-code, hermes und agents. hmgr harness list zeigt sie mit ihrem Stand.';
739
+ if (fehler instanceof QuelleUnbekannt)
740
+ return 'hmgr source list zeigt die bekannten Quellen.';
741
+ if (fehler instanceof QuelleNameBelegt)
742
+ return 'hmgr source list zeigt die schon vergebenen Namen.';
743
+ if (fehler instanceof ZeigerSchreibenGesperrt)
744
+ return 'Nutze --local, das schreibt nichts in den Zeiger.';
745
+ if (fehler instanceof SkillOrdnerFaehigkeitFehlt)
746
+ return 'Nur hermes kennt einen verwalteten Skill-Ordner.';
747
+ if (fehler instanceof SkillOrdnerBereitsVerwaltet)
748
+ return `Erst entfernen: hmgr harness skills-dir ${fehler.ziel.harness} remove${fehler.ziel.ort.art === 'profil' ? ` --profil ${fehler.ziel.ort.name}` : ''}.`;
749
+ if (fehler instanceof SkillOrdnerNichtVerwaltet)
750
+ return `Festlegen mit hmgr harness skills-dir ${fehler.ziel.harness} add <pfad>${fehler.ziel.ort.art === 'profil' ? ` --profil ${fehler.ziel.ort.name}` : ''}.`;
751
+ if (fehler instanceof SkillOrdnerUngueltig)
752
+ return fehler.grund === 'ausserhalb'
753
+ ? 'Der Pfad muss unterhalb des Hermes-Home liegen.'
754
+ : 'Das ist Hermes\' eigener Skill-Ordner, ein anderer Pfad muss her.';
409
755
  return undefined;
410
756
  }
411
757
  export async function main(argv, umgebung) {