hmgr 0.0.1

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.
@@ -0,0 +1,25 @@
1
+ // Lokaler Zustand als eine JSON-Datei, Standard ~/.hmgr/state.json (E29).
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { leererZustand } from "../ports/zustand.js";
5
+ export class ZustandJson {
6
+ pfad;
7
+ constructor(pfad) {
8
+ this.pfad = pfad;
9
+ }
10
+ async lesen() {
11
+ const roh = await readFile(this.pfad, 'utf8').catch(() => undefined);
12
+ if (roh === undefined)
13
+ return leererZustand();
14
+ const daten = JSON.parse(roh);
15
+ return {
16
+ quellen: (daten.quellen ?? []),
17
+ arbeitsverzeichnisse: (daten.arbeitsverzeichnisse ?? []),
18
+ bindungen: (daten.bindungen ?? []),
19
+ };
20
+ }
21
+ async schreiben(zustand) {
22
+ await mkdir(path.dirname(this.pfad), { recursive: true });
23
+ await writeFile(this.pfad, `${JSON.stringify(zustand, null, 2)}\n`);
24
+ }
25
+ }
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+ // hmgr-CLI ohne Bibliothek: argv von Hand, ein kleines Kommando.
3
+ import path from 'node:path';
4
+ import { realpathSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { pathToFileURL } from 'node:url';
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";
9
+ import { ClaudeCode } from "../adapter/claude-code.js";
10
+ import { Hermes } from "../adapter/hermes.js";
11
+ import { ComposerZeiger } from "../adapter/composer.js";
12
+ import { DateisystemZiele } from "../adapter/dateisystem.js";
13
+ import { GitQuellen } from "../adapter/git.js";
14
+ import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
15
+ import { ZustandJson } from "../adapter/zustand-json.js";
16
+ export function umgebungLesen(env = process.env) {
17
+ const home = env.HOME ?? homedir();
18
+ return { home, hmgrHome: env.HMGR_HOME ?? path.join(home, '.hmgr') };
19
+ }
20
+ export function bauen(umgebung, optionen = {}) {
21
+ const zustand = new ZustandJson(path.join(umgebung.hmgrHome, 'state.json'));
22
+ const wurzel = path.join(umgebung.hmgrHome, 'checkouts');
23
+ const claude = optionen.claudeAusfuehrer === undefined
24
+ ? new ClaudeCode(umgebung.home)
25
+ : new ClaudeCode(umgebung.home, optionen.claudeAusfuehrer);
26
+ const ports = {
27
+ harnesses: [claude, new Hermes(umgebung.home)],
28
+ quellen: new GitQuellen(wurzel),
29
+ ziele: new DateisystemZiele(wurzel, zustand),
30
+ zeiger: new ComposerZeiger(),
31
+ zustand,
32
+ };
33
+ return { anwendungsfaelle: new Anwendungsfaelle(ports), ports };
34
+ }
35
+ /** Schalter ohne Wert. Alles andere braucht einen. */
36
+ const FLAGGEN = new Set(['ersetzen', 'local', 'lokal', 'probelauf']);
37
+ function schalterLesen(args) {
38
+ const rest = [];
39
+ const werte = new Map();
40
+ const flaggen = new Set();
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i];
43
+ if (!arg.startsWith('--')) {
44
+ rest.push(arg);
45
+ continue;
46
+ }
47
+ const name = arg.slice(2);
48
+ if (FLAGGEN.has(name)) {
49
+ flaggen.add(name);
50
+ continue;
51
+ }
52
+ const wert = args[++i];
53
+ if (wert === undefined)
54
+ throw new Error(`Schalter --${name} braucht einen Wert`);
55
+ werte.set(name, wert);
56
+ }
57
+ return { rest, werte, flaggen };
58
+ }
59
+ function ortLesen(text) {
60
+ const roh = text ?? 'home';
61
+ if (roh === 'home')
62
+ return { art: 'home' };
63
+ if (roh.startsWith('dir:'))
64
+ return { art: 'arbeitsverzeichnis', pfad: path.resolve(roh.slice(4)) };
65
+ if (roh.startsWith('profil:'))
66
+ return { art: 'profil', name: roh.slice(7) };
67
+ throw new Error(`Unbekannter Ort „${roh}", erwartet home, dir:<pfad> oder profil:<name>`);
68
+ }
69
+ const HILFE = `hmgr <kommando>
70
+
71
+ source add <url|pfad> Quelle hinzufügen (URL wird geklont, Pfad bleibt Arbeitskopie)
72
+ source list Quellen zeigen
73
+ dir add <pfad> Arbeitsverzeichnis merken und an seinen Zeiger binden
74
+ dir forget <pfad> Arbeitsverzeichnis und Bindung vergessen, Links bleiben
75
+ dir list Arbeitsverzeichnisse mit ihrer Bindung zeigen
76
+ link <skill> Skill in ein Ziel verlinken
77
+ unlink <skill> Link entfernen
78
+ adopt [name] Fremde Links im Ziel durch eigene hmgr-Links ersetzen
79
+ plugin add <name> Plugin in ein Ziel legen (installieren oder Skills verlinken)
80
+ plugin remove <name> Plugin aus einem Ziel entfernen
81
+ plugin list Plugins der Marketplaces aller Quellen
82
+ sync [pfad] Ziel auf den Stand seines Zeigers bringen
83
+ update Alle Checkouts aktualisieren
84
+ status Ist-Zustand der Ziele
85
+
86
+ --harness <name> claude-code oder hermes, Standard claude-code
87
+ --ort home|dir:<pfad>|profil:<name> Standard home
88
+ --quelle <teilstring> Quelle bei mehrdeutigem Skill-Namen
89
+ --version <spanne> Versionseinschränkung nach Composer-Regeln, nur mit Zeiger (E33)
90
+ --composer <pfad> composer.json wählen, wenn mehrere in Frage kommen
91
+ --local Link nur ins Ziel legen, nicht in den Zeiger
92
+ --ersetzen fremden Eintrag am Ziel ersetzen
93
+ --probelauf bei adopt nur melden, nichts schreiben`;
94
+ /** Klartext für den Grund, aus dem ein fremder Eintrag liegen bleibt (B12). */
95
+ const UEBERNAHME_GRUND = {
96
+ 'kein-link': 'kein Symlink',
97
+ 'keine-arbeitskopie': 'liegt in keiner Git-Arbeitskopie',
98
+ 'keine-url': 'Arbeitskopie ohne remote.origin.url',
99
+ 'kein-branch': 'Arbeitskopie ohne Branch (detached HEAD)',
100
+ 'skill-unbekannt': 'Pfad ist in der Quelle kein Skill',
101
+ };
102
+ export async function ausfuehren(argv, umgebung, optionen = {}) {
103
+ const { anwendungsfaelle, ports } = bauen(umgebung, optionen);
104
+ const kommando = argv[0];
105
+ if (kommando === undefined || kommando === 'help' || kommando === '--help') {
106
+ console.log(HILFE);
107
+ return kommando === undefined ? 1 : 0;
108
+ }
109
+ if (kommando === 'source') {
110
+ const unterkommando = argv[1];
111
+ if (unterkommando === 'add') {
112
+ const text = argv[2];
113
+ if (text === undefined)
114
+ throw new Error('source add braucht eine URL oder einen Pfad');
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}`);
120
+ if (marketplace !== null) {
121
+ console.log(`Marketplace ${marketplace.name}, ${marketplace.plugins.length} Plugins:`);
122
+ for (const plugin of marketplace.plugins) {
123
+ const abh = plugin.dependencies.length === 0 ? '' : ` braucht ${plugin.dependencies.join(', ')}`;
124
+ console.log(` ${plugin.name} ${plugin.skills.map((s) => s.pfad).join(', ')}${abh}`);
125
+ }
126
+ }
127
+ return 0;
128
+ }
129
+ if (unterkommando === 'list') {
130
+ const zustand = await ports.zustand.lesen();
131
+ if (zustand.quellen.length === 0)
132
+ console.log('Keine Quellen.');
133
+ for (const quelle of zustand.quellen)
134
+ console.log(`${quelle.art} ${quelleAlsText(quelle)}`);
135
+ return 0;
136
+ }
137
+ throw new Error('source add <url|pfad> oder source list');
138
+ }
139
+ if (kommando === 'dir') {
140
+ const { rest, werte } = schalterLesen(argv.slice(1));
141
+ const unterkommando = rest[0];
142
+ if (unterkommando === 'add') {
143
+ const pfad = rest[1];
144
+ if (pfad === undefined)
145
+ throw new Error('dir add braucht einen Pfad');
146
+ const composer = werte.get('composer');
147
+ const ergebnis = composer === undefined
148
+ ? await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)
149
+ : await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad, composer);
150
+ console.log(`Arbeitsverzeichnis ${ergebnis.pfad}`);
151
+ if (ergebnis.zeigerPfad !== undefined)
152
+ console.log(` Zeiger ${ergebnis.zeigerPfad}`);
153
+ if (ergebnis.hinweis !== undefined)
154
+ console.log(` ${ergebnis.hinweis}`);
155
+ return 0;
156
+ }
157
+ if (unterkommando === 'forget') {
158
+ const pfad = rest[1];
159
+ if (pfad === undefined)
160
+ throw new Error('dir forget braucht einen Pfad');
161
+ await anwendungsfaelle.arbeitsverzeichnisVergessen(pfad);
162
+ console.log(`Arbeitsverzeichnis ${path.resolve(pfad)} vergessen, die Links bleiben liegen.`);
163
+ return 0;
164
+ }
165
+ if (unterkommando === 'list') {
166
+ const zustand = await ports.zustand.lesen();
167
+ if (zustand.arbeitsverzeichnisse.length === 0)
168
+ console.log('Keine Arbeitsverzeichnisse.');
169
+ for (const pfad of zustand.arbeitsverzeichnisse) {
170
+ const bindung = zustand.bindungen.find((b) => b.ziel.ort.art === 'arbeitsverzeichnis' && b.ziel.ort.pfad === pfad);
171
+ console.log(`${pfad} ${bindung === undefined ? 'ohne Zeiger' : bindung.zeigerPfad}`);
172
+ }
173
+ return 0;
174
+ }
175
+ throw new Error('dir add <pfad>, dir forget <pfad> oder dir list');
176
+ }
177
+ if (kommando === 'link' || kommando === 'unlink') {
178
+ const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
179
+ const skill = rest[0];
180
+ if (skill === undefined)
181
+ throw new Error(`${kommando} braucht einen Skill-Namen`);
182
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
183
+ const lokal = flaggen.has('local') || flaggen.has('lokal');
184
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
185
+ // Verzeichnis merken und binden, damit status es wiederfindet und der Zeiger bekannt ist (E21).
186
+ const composer = werte.get('composer');
187
+ if (composer === undefined)
188
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
189
+ else
190
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
191
+ }
192
+ if (kommando === 'unlink') {
193
+ await anwendungsfaelle.linkEntfernen({ skill, ziel, lokal });
194
+ console.log(`Link ${skill} entfernt.`);
195
+ return 0;
196
+ }
197
+ const quelle = werte.get('quelle');
198
+ const version = werte.get('version');
199
+ const link = await anwendungsfaelle.linkLegen({
200
+ skill,
201
+ ziel,
202
+ ...(quelle === undefined ? {} : { quelle }),
203
+ ...(version === undefined ? {} : { version }),
204
+ ersetzen: flaggen.has('ersetzen'),
205
+ lokal,
206
+ });
207
+ const spanne = version === undefined ? '' : `${version} → `;
208
+ console.log(`${link.name} -> ${spanne}${quelleAlsText(link.checkout.quelle)} @ ${link.checkout.ref} (${link.skill.pfad})`);
209
+ if (link.hinweis !== undefined)
210
+ console.log(link.hinweis);
211
+ return 0;
212
+ }
213
+ if (kommando === 'adopt') {
214
+ const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
215
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
216
+ const probelauf = flaggen.has('probelauf');
217
+ const name = rest[0];
218
+ if (ziel.ort.art === 'arbeitsverzeichnis' && !probelauf) {
219
+ const composer = werte.get('composer');
220
+ if (composer === undefined)
221
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
222
+ else
223
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
224
+ }
225
+ const ergebnis = await anwendungsfaelle.uebernehmen({
226
+ ziel,
227
+ ...(name === undefined ? {} : { name }),
228
+ probelauf,
229
+ lokal: flaggen.has('local') || flaggen.has('lokal'),
230
+ });
231
+ if (probelauf)
232
+ console.log('Probelauf, es wird nichts geschrieben.');
233
+ let uebernommen = 0;
234
+ let uebersprungen = 0;
235
+ let neueQuellen = 0;
236
+ for (const eintrag of ergebnis.eintraege) {
237
+ if (eintrag.ergebnis === 'uebersprungen') {
238
+ uebersprungen++;
239
+ console.log(` übersprungen ${eintrag.name} ${UEBERNAHME_GRUND[eintrag.grund]}`);
240
+ continue;
241
+ }
242
+ uebernommen++;
243
+ if (eintrag.quelleNeu)
244
+ neueQuellen++;
245
+ const quelleNeu = eintrag.quelleNeu ? ' (neue Quelle)' : '';
246
+ console.log(` übernommen ${eintrag.name} ${quelleAlsText(eintrag.quelle)} @ ${eintrag.ref} (${eintrag.skill.pfad})${quelleNeu}`);
247
+ }
248
+ const quellenText = neueQuellen === 1 ? '1 neue Quelle' : `${neueQuellen} neue Quellen`;
249
+ console.log(`${uebernommen} übernommen, ${uebersprungen} übersprungen, ${quellenText}.`);
250
+ return 0;
251
+ }
252
+ if (kommando === 'plugin') {
253
+ const { rest, werte, flaggen } = schalterLesen(argv.slice(1));
254
+ const unterkommando = rest[0];
255
+ if (unterkommando === 'list') {
256
+ const marketplaces = await anwendungsfaelle.marketplaces();
257
+ if (marketplaces.length === 0)
258
+ console.log('Keine Quelle mit Marketplace.');
259
+ for (const marketplace of marketplaces) {
260
+ console.log(`Marketplace ${marketplace.name} (${quelleAlsText(marketplace.quelle)})`);
261
+ for (const plugin of marketplace.plugins) {
262
+ console.log(` ${plugin.name}`);
263
+ for (const skill of plugin.skills)
264
+ console.log(` Skill ${skill.name} ${skill.pfad}`);
265
+ for (const abhaengigkeit of plugin.dependencies)
266
+ console.log(` braucht ${abhaengigkeit}`);
267
+ }
268
+ }
269
+ return 0;
270
+ }
271
+ if (unterkommando !== 'add' && unterkommando !== 'remove')
272
+ throw new Error('plugin add <name>, plugin remove <name> oder plugin list');
273
+ const name = rest[1];
274
+ if (name === undefined)
275
+ throw new Error(`plugin ${unterkommando} braucht einen Plugin-Namen`);
276
+ const ziel = { harness: werte.get('harness') ?? 'claude-code', ort: ortLesen(werte.get('ort')) };
277
+ const lokal = flaggen.has('local') || flaggen.has('lokal');
278
+ if (ziel.ort.art === 'arbeitsverzeichnis') {
279
+ const composer = werte.get('composer');
280
+ if (composer === undefined)
281
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad);
282
+ else
283
+ await anwendungsfaelle.arbeitsverzeichnisHinzufuegen(ziel.ort.pfad, composer);
284
+ }
285
+ const quelle = werte.get('quelle');
286
+ const ergebnis = unterkommando === 'add'
287
+ ? await anwendungsfaelle.pluginLegen({ plugin: name, ziel, ...(quelle === undefined ? {} : { quelle }), lokal })
288
+ : await anwendungsfaelle.pluginEntfernen({ plugin: name, ziel, lokal });
289
+ const verb = unterkommando === 'add' ? 'gelegt' : 'entfernt';
290
+ console.log(`Plugin ${pluginSchluessel(ergebnis.plugin)} ${verb} (${ergebnis.art})`);
291
+ if (ergebnis.skills.length > 0)
292
+ console.log(` Skills: ${ergebnis.skills.join(', ')}`);
293
+ if (ergebnis.imZeiger)
294
+ console.log(' im Zeiger vermerkt');
295
+ if (ergebnis.hinweis !== undefined)
296
+ console.log(` ${ergebnis.hinweis}`);
297
+ return 0;
298
+ }
299
+ if (kommando === 'sync') {
300
+ const { rest } = schalterLesen(argv.slice(1));
301
+ const ergebnis = await anwendungsfaelle.abgleich(rest[0]);
302
+ if (ergebnis.zeigerPfad !== undefined)
303
+ console.log(`Zeiger ${ergebnis.zeigerPfad}`);
304
+ for (const zeile of ergebnis.gelegt)
305
+ console.log(` gelegt ${zeile}`);
306
+ for (const zeile of ergebnis.vorhanden)
307
+ console.log(` vorhanden ${zeile}`);
308
+ for (const zeile of ergebnis.abweichend)
309
+ console.log(` abweichend ${zeile}`);
310
+ for (const zeile of ergebnis.fehler)
311
+ console.log(` Fehler ${zeile}`);
312
+ for (const zeile of ergebnis.hinweise)
313
+ console.log(zeile);
314
+ return 0;
315
+ }
316
+ if (kommando === 'update') {
317
+ const { checkouts: liste, pluginHarnesses } = await anwendungsfaelle.aktualisieren();
318
+ if (liste.length === 0)
319
+ console.log('Keine Checkouts.');
320
+ for (const { checkout, aktualisierung } of liste) {
321
+ const kopf = `${quelleAlsText(checkout.quelle)} @ ${checkout.ref}`;
322
+ if (aktualisierung.ergebnis === 'aktualisiert')
323
+ console.log(` aktualisiert ${kopf} ${aktualisierung.von.slice(0, 8)} -> ${aktualisierung.nach.slice(0, 8)}`);
324
+ else if (aktualisierung.ergebnis === 'unveraendert')
325
+ console.log(` unverändert ${kopf}`);
326
+ else
327
+ console.log(` übersprungen ${kopf} lokale Änderung (E15)`);
328
+ }
329
+ for (const name of pluginHarnesses)
330
+ console.log(` Plugins ${name} aktualisiert (E14)`);
331
+ return 0;
332
+ }
333
+ if (kommando === 'status') {
334
+ for (const zielZustand of await anwendungsfaelle.istZustand()) {
335
+ console.log(`Ziel ${zielZustand.ziel.harness} ${beschreibeOrt(zielZustand.ziel.ort)} (${zielZustand.verzeichnis})`);
336
+ if (zielZustand.eintraege.length === 0)
337
+ console.log(' (leer)');
338
+ for (const eintrag of zielZustand.eintraege) {
339
+ if (eintrag.art === 'fremd')
340
+ console.log(` ${eintrag.name} fremd`);
341
+ else if (eintrag.art === 'link-ohne-quelle')
342
+ console.log(` ${eintrag.name} Link ohne bekannte Quelle (${eintrag.checkoutPfad})`);
343
+ else {
344
+ // Steht im Zeiger eine Versionseinschränkung, kommt sie vor den aufgelösten Ref (E33).
345
+ const einschraenkung = zielZustand.einschraenkungen.get(eintrag.link.name);
346
+ const spanne = einschraenkung === undefined ? '' : `${einschraenkung} → `;
347
+ console.log(` ${eintrag.link.name} ${spanne}${quelleAlsText(eintrag.link.checkout.quelle)} @ ${eintrag.link.checkout.ref} (${eintrag.link.skill.pfad})`);
348
+ }
349
+ }
350
+ if (zielZustand.plugins !== undefined)
351
+ console.log(` Plugins: ${zielZustand.plugins.length === 0 ? 'keine' : zielZustand.plugins.join(', ')}`);
352
+ }
353
+ const fehlend = await anwendungsfaelle.nichtVorhandeneHarnesses();
354
+ if (fehlend.length > 0)
355
+ console.log(`Nicht vorhanden: ${fehlend.join(', ')}`);
356
+ return 0;
357
+ }
358
+ throw new Error(`Unbekanntes Kommando „${kommando}"`);
359
+ }
360
+ function beschreibeOrt(ort) {
361
+ if (ort.art === 'home')
362
+ return 'home';
363
+ if (ort.art === 'profil')
364
+ return `profil:${ort.name}`;
365
+ return `dir:${ort.pfad}`;
366
+ }
367
+ function istModellFehler(fehler) {
368
+ return (fehler instanceof NameBelegt ||
369
+ fehler instanceof SkillMehrdeutig ||
370
+ fehler instanceof SkillUnbekannt ||
371
+ fehler instanceof OrtNichtUnterstuetzt ||
372
+ fehler instanceof ZeigerMehrdeutig ||
373
+ fehler instanceof HarnessNichtVorhanden ||
374
+ fehler instanceof KeinPassenderRef ||
375
+ fehler instanceof EinschraenkungOhneZeiger ||
376
+ fehler instanceof PluginMehrdeutig ||
377
+ fehler instanceof PluginUnbekannt ||
378
+ fehler instanceof AbhaengigkeitFehlt);
379
+ }
380
+ export async function main(argv, umgebung) {
381
+ try {
382
+ return await ausfuehren(argv, umgebung);
383
+ }
384
+ catch (fehler) {
385
+ const text = fehler instanceof Error ? fehler.message : String(fehler);
386
+ console.error(istModellFehler(fehler) ? text : `Fehler: ${text}`);
387
+ return 1;
388
+ }
389
+ }
390
+ /**
391
+ * Direkt gestartet oder importiert. Über den Realpath, weil `npm install -g` einen Symlink in
392
+ * `bin/` legt: argv[1] ist dann der Symlink, import.meta.url schon sein Ziel.
393
+ */
394
+ function direktGestartet() {
395
+ const einstieg = process.argv[1];
396
+ if (einstieg === undefined)
397
+ return false;
398
+ try {
399
+ return import.meta.url === pathToFileURL(realpathSync(einstieg)).href;
400
+ }
401
+ catch {
402
+ return false;
403
+ }
404
+ }
405
+ if (direktGestartet()) {
406
+ process.exitCode = await main(process.argv.slice(2), umgebungLesen());
407
+ }