kimal3 0.1.1 → 0.1.2

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/api.js CHANGED
@@ -40,12 +40,9 @@ export function feld(koerper, name) {
40
40
  }
41
41
  return null;
42
42
  }
43
- /** Dasselbe fuer eine Liste von Texten (fuer `projekt liste`). */
44
- export function textListe(koerper, name) {
45
- if (koerper && typeof koerper === "object" && name in koerper) {
46
- const w = koerper[name];
47
- if (Array.isArray(w) && w.every((x) => typeof x === "string"))
48
- return w;
49
- }
50
- return null;
51
- }
43
+ // `textListe` stand hier bis zum 2026-08-18 und zog EIN Textfeld je Eintrag --
44
+ // genau das war die Grenze, an der `projekt liste` haengen blieb (Befund L-4).
45
+ // Die Liste zeigt jetzt mehrere Felder je Projekt und liest ihre Antwort in
46
+ // `projekt-zeilen.ts`, wo `check:cli` sie messen kann. Eine Funktion, die
47
+ // niemand mehr ruft, wird nicht aufgehoben: Sie waere beim naechsten Mal die
48
+ // naheliegende, aber zu enge Wahl.
package/dist/argumente.js CHANGED
@@ -7,22 +7,50 @@
7
7
  * Titel anzulegen.
8
8
  */
9
9
  export function zerlege(argv, erlaubt) {
10
- const befehl = [];
11
10
  const optionen = {};
12
11
  for (let i = 0; i < argv.length; i++) {
13
12
  const teil = argv[i];
13
+ // ⚠️ **Jedes Wort ohne `--` ist hier ein Fehler** (B-4). `zerlege` bekommt
14
+ // vom Verteiler nur den Teil NACH dem Befehlsnamen -- dort gehoert nichts
15
+ // hin ausser Optionen. Frueher landeten solche Woerter in `werte.befehl`
16
+ // und wurden nie wieder angesehen.
14
17
  if (!teil.startsWith("--")) {
15
- if (Object.keys(optionen).length > 0) {
16
- return { ok: false, meldung: `Nach Optionen kommt kein Befehlswort mehr: „${teil}".` };
17
- }
18
- befehl.push(teil);
19
- continue;
18
+ return {
19
+ ok: false,
20
+ meldung: `Mit „${teil}" kann ich hier nichts anfangen. Erwartet werden nur Optionen wie „--name wert".`,
21
+ };
20
22
  }
21
23
  const gleich = teil.indexOf("=");
22
24
  const name = gleich === -1 ? teil.slice(2) : teil.slice(2, gleich);
23
25
  if (!erlaubt.includes(name)) {
24
26
  return { ok: false, meldung: `Die Option „--${name}" kenne ich nicht.` };
25
27
  }
28
+ // ⚠️ **Dieselbe Option zweimal ist ein FEHLER** (QA-Befund B-3, 2026-08-18).
29
+ //
30
+ // `optionen` ist ein `Record` -- die zweite Nennung ueberschrieb die erste
31
+ // stillschweigend. `kimal3 firma daten --firma "A" --firma "B"` lieferte
32
+ // die Daten von B, mit Rueckgabewert 0 und ohne jeden Hinweis. Wer die
33
+ // Antwort danach "fuer A" ueberschreibt, hat eine falsche Antwort auf eine
34
+ // richtige Frage.
35
+ //
36
+ // 🔑 Die Route ist gegen den Fall gewappnet (`getAll("firma").length > 1`,
37
+ // gebaut nach dem Absturz aus PR #14) -- nur bekam sie ihn ueber den
38
+ // CLI-Weg nie zu sehen: Hier wurde aus zwei Nennungen laengst eine.
39
+ // *Ein Schutz an der zweiten Tuer nuetzt nichts, wenn die erste das
40
+ // Problem schon weggeraeumt hat -- und man sieht ihm das beim Lesen nicht
41
+ // an.* Deshalb steht die Strenge HIER, wo sie alle Befehle auf einmal
42
+ // erreicht, und nicht in einem einzelnen Befehl.
43
+ //
44
+ // `Object.hasOwn` und nicht `name in optionen`: Letzteres findet auch
45
+ // geerbte Namen wie `constructor` und wuerde `--constructor` faelschlich
46
+ // als doppelt melden. Heute faengt `erlaubt` das ab -- aber diese Zeile
47
+ // soll auch dann noch stimmen, wenn jemand die Reihenfolge aendert.
48
+ if (Object.hasOwn(optionen, name)) {
49
+ return {
50
+ ok: false,
51
+ meldung: `Die Option „--${name}" steht zweimal da. Bitte nenne sie nur einmal.`,
52
+ };
53
+ }
26
54
  if (gleich !== -1) {
27
55
  optionen[name] = teil.slice(gleich + 1);
28
56
  continue;
@@ -35,5 +63,5 @@ export function zerlege(argv, erlaubt) {
35
63
  optionen[name] = wert;
36
64
  i++;
37
65
  }
38
- return { ok: true, werte: { befehl, optionen } };
66
+ return { ok: true, werte: { optionen } };
39
67
  }
@@ -0,0 +1,113 @@
1
+ import { zerlege } from "../argumente.js";
2
+ import { feld, ruf } from "../api.js";
3
+ import { schlussFehler } from "../fehler.js";
4
+ import { firmenZeilen } from "../firmen-liste.js";
5
+ import { BASIS_URL } from "../anmeldung.js";
6
+ const ERLAUBT = ["firma"];
7
+ /**
8
+ * Die Firmendaten holen — als JSON, unveraendert.
9
+ *
10
+ * 🔑 **Dieser Befehl INTERPRETIERT NICHTS. Er transportiert.** Die Auswertung
11
+ * macht Claude, gefuehrt vom Skill — das ist die KI-native Bauform und der
12
+ * Grund, warum es keine `--abteilung`- oder `--zeitraum`-Optionen gibt: Filtern
13
+ * kann Claude aus dem JSON selbst, und jede Option waere eine zweite Stelle fuer
14
+ * dieselbe Regel.
15
+ *
16
+ * ⚠️ Auf stdout geht das JSON und SONST NICHTS. Wer hier eine freundliche
17
+ * Ueberschrift dazudruckt, macht die Ausgabe als JSON unlesbar — alles fuer
18
+ * Menschen gehoert nach stderr (`console.error`).
19
+ */
20
+ const HILFE = `
21
+ kimal3 firma daten [--firma "Name"]
22
+
23
+ Holt die Daten deiner Firma als JSON: Kennzahlen, Menschen, Projekte.
24
+ Die Auswertung macht Claude daraus selbst.
25
+
26
+ --firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
27
+ Die Absage nennt dir die Namen als fertige Befehle.
28
+ `;
29
+ export async function firmaDaten(argv) {
30
+ // ⚠️ VOR `zerlege`, genau wie bei `projekt neu`: Ein Schalter ist keine
31
+ // Option-mit-Wert, `--help` scheiterte dort sonst mit "fehlt ihr Wert".
32
+ // Die Terminal-Hilfe wirbt mit "Mehr zu einem Befehl: … --help" -- ein
33
+ // Befehl, der darauf mit einem Fehler antwortet, macht die Werbung zur Luege.
34
+ if (argv.includes("--help") || argv.includes("-h")) {
35
+ console.log(HILFE);
36
+ return 0;
37
+ }
38
+ const zerlegt = zerlege(argv, ERLAUBT);
39
+ if (!zerlegt.ok) {
40
+ console.error(zerlegt.meldung);
41
+ return 1;
42
+ }
43
+ // ⚠️ `!== undefined` und NICHT auf Wahrheitswert pruefen: `zerlege`
44
+ // unterscheidet ausdruecklich "nicht angegeben" (`undefined`) von
45
+ // "ausdruecklich leer" (`--firma=` -> `""`), und `check:cli` sichert genau
46
+ // das ab. Wer hier auf Wahrheit prueft, wirft beides zusammen: `--firma=`
47
+ // lieferte dann die Daten der einzigen Firma, als haette niemand gefragt.
48
+ const wunsch = zerlegt.werte.optionen.firma;
49
+ const pfad = wunsch !== undefined
50
+ ? `/api/cli/firma?firma=${encodeURIComponent(wunsch)}`
51
+ : "/api/cli/firma";
52
+ const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true });
53
+ if (antwort.status === 200) {
54
+ // 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg (Review-Befund Runde 1):
55
+ // `ruf` verschluckt Parse-Fehler zu `null`. Ein Zwischending im Netz --
56
+ // etwa ein WLAN-Anmeldeportal -- antwortet mit `200` und HTML. Ohne diese
57
+ // Wache druckte der Befehl das Wort `null` auf stdout und meldete 0.
58
+ // Und die 0 ist die Schnittstelle: Claude liest sie als "erledigt" und
59
+ // haelt `null` fuer die Daten der Firma.
60
+ if (antwort.koerper === null || typeof antwort.koerper !== "object") {
61
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
62
+ return 3;
63
+ }
64
+ console.log(JSON.stringify(antwort.koerper, null, 2));
65
+ return 0;
66
+ }
67
+ const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
68
+ // 402 VOR 401: Beide heissen "kommst hier nicht rein", verlangen aber
69
+ // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` --
70
+ // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
71
+ if (antwort.status === 402) {
72
+ console.error(meldung);
73
+ return 4;
74
+ }
75
+ if (antwort.status === 401) {
76
+ console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
77
+ return 2;
78
+ }
79
+ if (antwort.status === 409) {
80
+ const grund = feld(antwort.koerper, "grund");
81
+ const koerper = antwort.koerper;
82
+ const namen = Array.isArray(koerper?.firmen)
83
+ ? koerper.firmen.filter((x) => typeof x === "string")
84
+ : [];
85
+ console.error(meldung);
86
+ // Die Namen als FERTIGE Befehle ausgeben, nicht als blosse Liste: Der
87
+ // naechste Schritt ist dann Kopieren statt Abtippen -- und ein Firmenname
88
+ // mit Leerzeichen braucht Anfuehrungszeichen, an die sonst niemand denkt.
89
+ //
90
+ // ⚠️ Welche Namen einen Befehl bekommen, entscheidet `firmenZeilen` (B-5):
91
+ // Ein doppelt vergebener Name bekommt KEINEN -- er waere ein Vorschlag,
92
+ // der garantiert wieder in derselben Absage endet.
93
+ const zeilen = firmenZeilen(namen);
94
+ if (zeilen.length > 0) {
95
+ console.error("\n Deine Firmen:");
96
+ zeilen.forEach((z) => console.error(z));
97
+ }
98
+ if (grund === "keine-firma") {
99
+ console.error(`\n Eine Firma anlegen: ${BASIS_URL}/firma/neu`);
100
+ }
101
+ return 1;
102
+ }
103
+ // ⚠️ Die 503 landet HIER und nicht oben bei der 402: Rueckgabewert 3 heisst
104
+ // "nochmal versuchen kann helfen", 4 heisst laut SKILL.md ausdruecklich
105
+ // "nicht wiederholen". Ein Aussetzer gehoert in den ersten Topf.
106
+ //
107
+ // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
108
+ // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
109
+ // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
110
+ const schluss = schlussFehler(antwort.status, meldung);
111
+ console.error(schluss.text);
112
+ return schluss.wert;
113
+ }
@@ -1,4 +1,6 @@
1
- import { feld, ruf, textListe } from "../api.js";
1
+ import { feld, ruf } from "../api.js";
2
+ import { schlussFehler } from "../fehler.js";
3
+ import { MAX_PROJEKTE_ANZEIGE, antwortHatProjekteFeld, titelFeldLesen, projekteAusAntwort, projektZeilen, titelAusAlterAntwort, } from "../projekt-zeilen.js";
2
4
  import { BASIS_URL } from "../anmeldung.js";
3
5
  /**
4
6
  * Die eigenen Projekte anzeigen.
@@ -6,24 +8,98 @@ import { BASIS_URL } from "../anmeldung.js";
6
8
  * 🔑 Wofuer das da ist: Damit Claude VOR dem Eintragen nachsehen kann. Ohne
7
9
  * diesen Befehl steht dasselbe Projekt irgendwann zweimal auf einer
8
10
  * oeffentlichen Seite mit dem Namen eines Menschen darauf.
11
+ *
12
+ * ⚠️ Seit dem 2026-08-18 (Befund L-4) stehen auch Live-Datum und Zahlen da.
13
+ * Der Slash-Befehl `/k3liste` versprach das schon vorher woertlich -- und die
14
+ * Ausgabe gab nur Titel her. *Eine Anleitung, die mehr verspricht als das
15
+ * Werkzeug hergibt, laesst Claude entweder raten oder sich entschuldigen.*
9
16
  */
10
17
  export async function projektListe() {
11
18
  const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true });
12
19
  if (antwort.status === 200) {
13
- const titel = textListe(antwort.koerper, "titel");
14
- if (!titel) {
15
- console.error("Das hat nicht geklappt.");
20
+ // 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dieselbe Wache wie
21
+ // in `firma-daten.ts`: `ruf` verschluckt Parse-Fehler zu `null`, und ein
22
+ // WLAN-Anmeldeportal antwortet mit `200` und HTML.
23
+ const projekte = projekteAusAntwort(antwort.koerper);
24
+ if (!projekte) {
25
+ /*
26
+ 🔴 **B-7 in der GEGENRICHTUNG** (Befund L-7, 2026-08-18, im Live-Durchlauf
27
+ selbst hineingelaufen). Bisher wurde jede unlesbare Antwort gleich
28
+ behandelt: „Die Antwort war nicht lesbar. Bitte gleich noch einmal."
29
+
30
+ Es gibt aber einen Fall, in dem Wiederholen NIE hilft: Die CLI ist neuer
31
+ als die App. Dann antwortet der Server ordentlich — nur eben in der
32
+ alten Form, mit `titel` statt `projekte`. Das passiert bei jedem
33
+ Deployment im Fenster zwischen npm und Vercel, und es traf hier den
34
+ eigenen Entwickler.
35
+
36
+ *Ein Rat, der garantiert nicht hilft, ist schlimmer als kein Rat: Er
37
+ sieht aus wie ein Ausweg.* -- dieselbe Lehre wie bei B-5.
38
+
39
+ Die alte Form ist erkennbar, also wird sie auch bedient: Der Mensch
40
+ bekommt seine Liste, nur ohne die neuen Angaben. Nichts kaputt, nichts
41
+ zu wiederholen.
42
+ */
43
+ const alt = titelAusAlterAntwort(antwort.koerper);
44
+ if (alt) {
45
+ if (alt.length === 0) {
46
+ console.log("Du hast noch keine Projekte eingetragen.");
47
+ console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
48
+ return 0;
49
+ }
50
+ console.log(`\n Deine Projekte (${alt.length} von ${MAX_PROJEKTE_ANZEIGE}):`);
51
+ alt.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
52
+ console.log("\n (Diese Version der App liefert noch keine Zahlen zu den");
53
+ console.log(" Projekten. Du hast nichts falsch gemacht — sie kommen mit");
54
+ console.log(" dem nächsten Update der App von selbst dazu.)");
55
+ console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
56
+ return 0;
57
+ }
58
+ /*
59
+ 🔴 **Die dritte Richtung: die App ist NEUER als dieses Werkzeug**
60
+ (Review-Runde 4, 2026-08-18). Steht `projekte` in der Antwort und war
61
+ sie trotzdem nicht lesbar, hat ein Eintrag eine Form, die diese
62
+ Fassung nicht kennt. Das ist **stabil** -- Wiederholen aendert daran
63
+ nichts, bis aktualisiert wird. Vorher fiel es in denselben Topf wie
64
+ eine kaputte Antwort und riet "gleich noch einmal".
65
+ */
66
+ if (antwortHatProjekteFeld(antwort.koerper)) {
67
+ /*
68
+ 🔑 **Erst geben, was da ist** (Review-Runde 6, 2026-08-18). Die neue
69
+ Route liefert `titel` und `projekte` IMMER gemeinsam. Aendert eine
70
+ spaetere App-Fassung die Form eines Eintrags, sind die Titel trotzdem
71
+ lesbar -- sie dann NICHT zu zeigen und stattdessen nur "aktualisiere"
72
+ zu sagen, wirft Daten weg, die man in der Hand haelt.
73
+ *Wer dem Menschen etwas geben kann, gibt es ihm -- und sagt dazu,
74
+ was fehlt.*
75
+ */
76
+ const titel = titelFeldLesen(antwort.koerper);
77
+ if (titel && titel.length > 0) {
78
+ console.log(`\n Deine Projekte (${titel.length} von ${MAX_PROJEKTE_ANZEIGE}):`);
79
+ titel.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
80
+ console.log("\n (Die Zahlen dazu kann diese Fassung von kimal3 nicht lesen —");
81
+ console.log(" die App ist neuer als das Werkzeug. Du hast nichts falsch");
82
+ console.log(" gemacht; `npm install -g kimal3` holt sie.)");
83
+ console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
84
+ return 0;
85
+ }
86
+ console.error("Diese Fassung von kimal3 versteht die Antwort der App nicht.");
87
+ console.error("Du hast nichts falsch gemacht — das Werkzeug ist älter als die App.");
88
+ console.error(" npm install -g kimal3");
89
+ return 1;
90
+ }
91
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
16
92
  return 3;
17
93
  }
18
94
  // Eine leere Liste ist KEIN Fehler — sie ist eine gueltige Antwort auf
19
95
  // eine gueltige Frage. Deshalb 0 und ein Satz, kein Fehlertext.
20
- if (titel.length === 0) {
96
+ if (projekte.length === 0) {
21
97
  console.log("Du hast noch keine Projekte eingetragen.");
22
98
  console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
23
99
  return 0;
24
100
  }
25
- console.log(`\n Deine Projekte (${titel.length} von 12):`);
26
- titel.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
101
+ console.log(`\n Deine Projekte (${projekte.length} von ${MAX_PROJEKTE_ANZEIGE}):\n`);
102
+ projektZeilen(projekte).forEach((z) => console.log(z));
27
103
  console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
28
104
  return 0;
29
105
  }
@@ -48,6 +124,11 @@ export async function projektListe() {
48
124
  // versuchen kann helfen"; 4 heisst laut SKILL.md ausdruecklich "nicht
49
125
  // wiederholen". Ein Lesefehler an der Pro-Schranke gehoert in den ersten
50
126
  // Topf. (Review-Befund 2026-08-14.)
51
- console.error(meldung);
52
- return antwort.status >= 500 ? 3 : 1;
127
+ //
128
+ // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
129
+ // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
130
+ // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
131
+ const schluss = schlussFehler(antwort.status, meldung);
132
+ console.error(schluss.text);
133
+ return schluss.wert;
53
134
  }
@@ -1,4 +1,5 @@
1
1
  import { feld, ruf } from "../api.js";
2
+ import { schlussFehler } from "../fehler.js";
2
3
  import { BASIS_URL } from "../anmeldung.js";
3
4
  import { zerlege } from "../argumente.js";
4
5
  const OPTIONEN = [
@@ -77,6 +78,11 @@ export async function projektNeu(argv) {
77
78
  // versuchen kann helfen"; 4 heisst laut SKILL.md ausdruecklich "nicht
78
79
  // wiederholen". Ein Lesefehler an der Pro-Schranke gehoert in den ersten
79
80
  // Topf. (Review-Befund 2026-08-14.)
80
- console.error(meldung);
81
- return antwort.status >= 500 ? 3 : 1;
81
+ //
82
+ // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
83
+ // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
84
+ // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
85
+ const schluss = schlussFehler(antwort.status, meldung);
86
+ console.error(schluss.text);
87
+ return schluss.wert;
82
88
  }
@@ -1,22 +1,61 @@
1
- import { copyFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { join } from "node:path";
5
5
  /**
6
- * Legt das Skill GLOBAL ab, damit Claude dieses Werkzeug ueberall kennt.
6
+ * Namen, die frueher einmal ausgeliefert wurden und heute nicht mehr.
7
+ *
8
+ * 🔑 **Wofuer das da ist** (Review-Befund, 2026-08-18): Die Befehle landen im
9
+ * GEMEINSAMEN Ordner `~/.claude/commands/` und werden dort nur ueberschrieben,
10
+ * nie entfernt. Wird ein Befehl spaeter umbenannt oder gestrichen, ueberlebt
11
+ * die alte Datei jedes `skill install` und bietet weiter Anweisungen an, die
12
+ * auf einen nicht mehr vorhandenen Unterbefehl zeigen. `SKILL.md` kann das
13
+ * nicht passieren -- es ist EINE Datei, die ueberschrieben wird.
14
+ *
15
+ * ⚠️ Warum eine Liste und nicht "alles loeschen, was wir nicht ausliefern":
16
+ * Der Ordner gehoert dem Menschen. Ein `k3*.md`, das er selbst angelegt hat,
17
+ * duerfen wir nicht wegraeumen. **Wer einen Befehl umbenennt oder entfernt,
18
+ * traegt den alten Namen hier ein** -- sonst bleibt er beim Nutzer stehen.
19
+ */
20
+ const VERALTETE_BEFEHLE = [];
21
+ /**
22
+ * Woran wir unsere eigenen Befehlsdateien erkennen.
23
+ *
24
+ * Steht die Marke nicht drin, hat ein Mensch die Datei geschrieben -- dann
25
+ * wird sie NICHT ueberschrieben, sondern uebersprungen und gemeldet.
26
+ */
27
+ const MARKE = "kimal3:erzeugt";
28
+ /**
29
+ * Legt das Skill UND die Slash-Befehle GLOBAL ab, damit Claude dieses Werkzeug
30
+ * ueberall kennt -- und damit der Mensch es auch selbst ausloesen kann.
31
+ *
32
+ * 🔑 **Warum zwei Ziele und nicht eins** (Entscheidung Dennis, 2026-08-18):
33
+ * Das Skill ist der Ausloeser fuer CLAUDE ("das Projekt ist fertig" -> Claude
34
+ * erkennt die Absicht). Die Slash-Befehle sind der Ausloeser fuer den
35
+ * MENSCHEN ("/k3firma …" -> er muss sich keinen Satz merken). Das eine ist
36
+ * wahrscheinlich, das andere sicher. Wer nur das Skill installiert, laesst den
37
+ * sicheren Weg weg.
7
38
  *
8
39
  * Der Pfad zur eigenen Datei kommt ueber `import.meta.url` und NICHT ueber
9
40
  * `process.cwd()` — die Quelle liegt im installierten Paket, das Ziel im
10
41
  * Ordner des Menschen. Wer beides verwechselt, kopiert eine Datei auf sich
11
42
  * selbst.
12
43
  *
13
- * `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/SKILL.md`.
44
+ * `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/…`.
14
45
  */
15
46
  export async function skillInstall() {
16
47
  const quelle = fileURLToPath(new URL("../../skill/SKILL.md", import.meta.url));
17
48
  if (!existsSync(quelle)) {
18
- console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren.");
19
- return 3;
49
+ /*
50
+ 🔴 **`1`, nicht `3`** (Review-Befund, 2026-08-18). Ein unvollstaendiges
51
+ Paket wird durch Wiederholen nie vollstaendig. `3` heisst laut SKILL.md
52
+ ausdruecklich "einmal wiederholen" -- Claude liest die ZAHL, nicht den
53
+ Text, und wuerde einen Rat geben, der garantiert nicht hilft. Genau der
54
+ Fehlertyp, den dieser Zweig bei B-5, B-7 und L-7 behoben hat.
55
+ */
56
+ console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren:");
57
+ console.error(" npm install -g kimal3");
58
+ return 1;
20
59
  }
21
60
  // 🔑 GLOBAL, nicht projektlokal (Fund von Dennis, 2026-08-13).
22
61
  // Gemessen: In ~/.claude/skills liegen 34 Skills, im Projekt selbst kein
@@ -39,6 +78,232 @@ export async function skillInstall() {
39
78
  }
40
79
  console.log(gabEsSchon ? "✓ Skill aktualisiert." : "✓ Skill angelegt.");
41
80
  console.log(` ${ziel}`);
42
- console.log(" Claude kennt das Werkzeug ab der nächsten Sitzung.");
81
+ const namen = slashBefehleInstallieren();
82
+ if (namen === "kaputt") {
83
+ /*
84
+ ⚠️ **Was hier zaehlt, ist die TEILWIRKUNG** (Review-Runde 2, 2026-08-18):
85
+ Das Skill steht oben bereits als angelegt gemeldet. Ohne diesen Satz
86
+ liest der Mensch ein "✓" und danach einen Fehler und weiss nicht, was
87
+ nun gilt. *Ein halber Erfolg ist weder ein Erfolg noch ein Totalausfall
88
+ -- er ist ein eigener Zustand und braucht einen eigenen Satz.*
89
+ */
90
+ console.error(" Das Skill ist installiert, die Slash-Befehle sind es NICHT.");
91
+ return 1;
92
+ }
93
+ const { geschrieben, uebersprungen, unlesbar, gestoert } = namen;
94
+ if (geschrieben.length > 0) {
95
+ console.log(`✓ ${geschrieben.length} Slash-Befehle: ${geschrieben.map((n) => `/${n}`).join(" · ")}`);
96
+ console.log(` ${join(homedir(), ".claude", "commands")}`);
97
+ }
98
+ // ⚠️ Uebersprungene Dateien gehoeren in die AUSGABE, nicht nur in die Logik.
99
+ // Ein Mensch, dem stillschweigend ein Befehl fehlt, sucht den Fehler bei
100
+ // sich. (Review-Runde 2, 2026-08-18.)
101
+ if (uebersprungen.length > 0) {
102
+ console.log(`⚠ ${uebersprungen.length} übersprungen: ${uebersprungen.map((n) => `/${n}`).join(" · ")}`);
103
+ console.log(" Dort liegt bereits eine Datei, die nicht von kimal3 stammt —");
104
+ console.log(" sie bleibt unangetastet. Wer die kimal3-Fassung will, benennt");
105
+ console.log(" die eigene um und führt den Befehl erneut aus.");
106
+ }
107
+ // 🔴 Ein LESEFEHLER bekommt einen eigenen Satz (Review-Runde 6): Er sieht
108
+ // aus wie Fremdbesitz, ist aber keiner -- und der Rat "benenn deine Datei
109
+ // um" waere hier falsch.
110
+ if (unlesbar.length > 0) {
111
+ console.log(`⚠ ${unlesbar.length} nicht lesbar: ${unlesbar.map((n) => `/${n}`).join(" · ")}`);
112
+ console.log(" Diese Dateien liessen sich nicht öffnen — vielleicht sind sie");
113
+ console.log(" gerade in Benutzung. Sie wurden nicht angefasst; ein zweiter");
114
+ console.log(" Versuch gleich kann klappen.");
115
+ }
116
+ /*
117
+ 🔴 **Kein Slash-Befehl installiert ist kein Erfolg** (Review-Runde 5,
118
+ 2026-08-18). Liegen im Ordner lauter fremde `k3*.md`, landet ALLES in
119
+ `uebersprungen`, `geschrieben` ist leer -- und der Lauf endete trotzdem
120
+ auf `0`. Laut der eigenen Tabelle in `SKILL.md` heisst `0` "erledigt".
121
+
122
+ *Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall.*
123
+
124
+ ⚠️ Die Zahl haengt am GRUND (Review-Runde 6): War der Ordner nur belegt,
125
+ ist der Zustand stabil -> `1`. Ging das Schreiben schief oder war eine
126
+ Datei nicht lesbar, kann ein zweiter Versuch helfen -> `3`.
127
+ */
128
+ if (geschrieben.length === 0) {
129
+ const wiederholbar = gestoert || unlesbar.length > 0;
130
+ console.error(" Es wurde KEIN Slash-Befehl installiert.");
131
+ console.error(" Das Skill selbst ist installiert und funktioniert.");
132
+ return wiederholbar ? 3 : 1;
133
+ }
134
+ /*
135
+ ⚠️ **Eine unvollstaendige Installation als solche melden** (Review-Runde 6).
136
+ Scheitert `copyFileSync` bei Datei 3 von 4, sind zwei Befehle da -- der
137
+ Lauf ist weder gelungen noch gescheitert. Frueher warf der `catch` die
138
+ Teilwirkung weg und meldete alles als misslungen.
139
+ */
140
+ if (gestoert) {
141
+ console.error(" Nicht alle Slash-Befehle konnten geschrieben werden.");
142
+ console.error(" Die oben genannten stehen; ein zweiter Lauf holt den Rest.");
143
+ return 3;
144
+ }
145
+ /*
146
+ ⚠️ **Nicht auf einen Befehl verweisen, den wir gerade NICHT installiert
147
+ haben** (Review-Runde 3, 2026-08-18, gemessen). Mit einer fremden
148
+ `k3.md` im Ordner stand hier woertlich "Tipp /k3 in Claude Code" --
149
+ ausgerechnet der eine Befehl, den der Lauf soeben abgelehnt hatte.
150
+ *Ein Hinweis auf etwas, das es nicht gibt, sieht aus wie ein Fehler des
151
+ Menschen.*
152
+ */
153
+ if (geschrieben.includes("k3")) {
154
+ console.log(" Tipp /k3 in Claude Code. (Taucht nichts auf, einmal neu starten.)");
155
+ }
156
+ else {
157
+ console.log(` Tipp /${geschrieben[0]} in Claude Code. (Taucht nichts auf, einmal neu starten.)`);
158
+ }
43
159
  return 0;
44
160
  }
161
+ /**
162
+ * Kopiert die Slash-Befehle nach `~/.claude/commands/`.
163
+ *
164
+ * ⚠️ **FLACH, nicht in einen Unterordner** — und das ist keine Bequemlichkeit:
165
+ * Ein Unterordner `kimal3/` macht aus `/k3firma` ein `/kimal3:k3firma`. Dann
166
+ * findet man die Familie nicht mehr, indem man `/k3` tippt, und genau das war
167
+ * die Anforderung (Dennis, 2026-08-18): einmal `/k3` eingeben und alles sehen.
168
+ * Der gemeinsame Namensanfang ERSETZT hier den Ordner.
169
+ *
170
+ * Rueckgabe: die Namen ohne `.md` — oder `"kaputt"` (Paket unvollstaendig,
171
+ * Wiederholen hilft NIE) bzw. `"stoerung"` (Schreiben ging schief, Wiederholen
172
+ * kann helfen). *Zwei Fehlerfaelle, die Gegenteiliges verlangen, duerfen nicht
173
+ * denselben Namen tragen.*
174
+ */
175
+ function slashBefehleInstallieren() {
176
+ const quellOrdner = fileURLToPath(new URL("../../skill/commands", import.meta.url));
177
+ if (!existsSync(quellOrdner)) {
178
+ console.error("Die Slash-Befehle liegen nicht im Paket. Bitte kimal3 neu installieren.");
179
+ return "kaputt";
180
+ }
181
+ const dateien = readdirSync(quellOrdner).filter((n) => n.endsWith(".md")).sort();
182
+ if (dateien.length === 0) {
183
+ console.error("Im Paket liegt kein einziger Slash-Befehl. Bitte kimal3 neu installieren.");
184
+ return "kaputt";
185
+ }
186
+ const zielOrdner = join(homedir(), ".claude", "commands");
187
+ const geschrieben = [];
188
+ const uebersprungen = [];
189
+ const unlesbar = [];
190
+ let gestoert = false;
191
+ try {
192
+ mkdirSync(zielOrdner, { recursive: true });
193
+ for (const datei of dateien) {
194
+ const ziel = join(zielOrdner, datei);
195
+ /*
196
+ 🔴 **Fremde Dateien NICHT ueberschreiben** (Review-Runde 2, 2026-08-18).
197
+ Der Ordner gehoert dem Menschen. Der Kommentar bei VERALTETE_BEFEHLE
198
+ argumentiert, dass wir eine selbstgeschriebene `k3*.md` nicht loeschen
199
+ duerfen -- *Ueberschreiben zerstoert sie genauso gruendlich*, nur
200
+ unauffaelliger.
201
+
202
+ Woran wir unsere eigenen erkennen: an der Marke `kimal3:erzeugt`. Ein
203
+ blosser Inhaltsvergleich taugt nicht -- eine AELTERE Fassung von uns
204
+ unterscheidet sich auch, und die soll ja gerade ersetzt werden.
205
+ */
206
+ if (existsSync(ziel)) {
207
+ const vorhanden = leseWennMoeglich(ziel);
208
+ /*
209
+ ⚠️ **Ein LESEFEHLER ist kein Fremdbesitz** (Review-Runde 6,
210
+ 2026-08-18). Vorher wurde ein gescheitertes `readFileSync` zu einem
211
+ leeren Text -- dann fehlt die Marke zwangslaeufig, und der Mensch las
212
+ "dort liegt eine Datei, die nicht von kimal3 stammt" fuer eine Datei,
213
+ die kimal3 selbst geschrieben hat. *Ein Fehlerfall unter dem Namen
214
+ eines anderen, zum vierten Mal in diesem Zweig.*
215
+ */
216
+ if (vorhanden === null) {
217
+ unlesbar.push(datei);
218
+ continue;
219
+ }
220
+ if (!vorhanden.includes(MARKE)) {
221
+ uebersprungen.push(datei);
222
+ continue;
223
+ }
224
+ }
225
+ copyFileSync(join(quellOrdner, datei), ziel);
226
+ geschrieben.push(datei);
227
+ }
228
+ }
229
+ catch (fehler) {
230
+ /*
231
+ ⚠️ **Die Teilwirkung ueberlebt den Fehler** (Review-Runde 6, 2026-08-18).
232
+ Vorher warf dieser `catch` alles weg und meldete "stoerung" -- auch wenn
233
+ zwei von vier Befehlen bereits geschrieben waren. Der Mensch hoerte "die
234
+ Slash-Befehle sind NICHT installiert", waehrend die Haelfte dastand.
235
+ `geschrieben` und `uebersprungen` sind ausserhalb des `try` deklariert,
236
+ genau dafuer.
237
+ */
238
+ console.error(`Die Slash-Befehle liessen sich nicht alle schreiben: ${String(fehler)}`);
239
+ gestoert = true;
240
+ }
241
+ raeumeVeralteteAuf(zielOrdner, VERALTETE_BEFEHLE);
242
+ return {
243
+ geschrieben: geschrieben.map((n) => n.replace(/\.md$/, "")),
244
+ uebersprungen: uebersprungen.map((n) => n.replace(/\.md$/, "")),
245
+ unlesbar: unlesbar.map((n) => n.replace(/\.md$/, "")),
246
+ gestoert,
247
+ };
248
+ }
249
+ /** Liest eine Datei — `null`, wenn sie sich nicht lesen laesst. */
250
+ function leseWennMoeglich(pfad) {
251
+ try {
252
+ return readFileSync(pfad, "utf8");
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ }
258
+ /**
259
+ * Entfernt Befehlsdateien, die frueher einmal ausgeliefert wurden.
260
+ *
261
+ * 🔴 **Warum das eine eigene, exportierte Funktion ist** (Review-Runde 3,
262
+ * 2026-08-18): `VERALTETE_BEFEHLE` ist leer, also ist diese Schleife noch nie
263
+ * gelaufen -- und `check:cli` "belegte" sie nur ueber eine Textsuche. Genau
264
+ * die Falle aus der Projekt-Notiz *wache-am-echten-fehlerfall-pruefen*.
265
+ * Mit dem Zielordner als Parameter laesst sie sich gegen einen Wegwerf-Ordner
266
+ * mit einer echten Datei fahren.
267
+ *
268
+ * 🔴 **Und sie loescht NUR eigene Dateien** (Review-Runde 6, 2026-08-18).
269
+ * Vorher entfernte sie blind, was auf der Liste stand -- waehrend die
270
+ * Kopierschleife siebzig Zeilen hoeher genau solche Dateien verschont und dem
271
+ * Menschen sagt "sie bleibt unangetastet". *Zwei Haelften desselben Befehls
272
+ * mit gegensaetzlicher Haltung zum selben Ordner.* Wird `/k3liste` spaeter
273
+ * umbenannt, verloere ein Mensch mit eigener `k3liste.md` sie ohne Rueckfrage.
274
+ *
275
+ * Gibt zurueck, was wirklich entfernt wurde -- eine Behauptung waere hier
276
+ * genau das Falsche.
277
+ */
278
+ export function raeumeVeralteteAuf(zielOrdner, veraltet) {
279
+ const entfernt = [];
280
+ for (const eintrag of veraltet) {
281
+ /*
282
+ 🔴 **Beide Schreibweisen annehmen** (Review-Runde 5, 2026-08-18). Der
283
+ Doc-Block sagt "wer einen Befehl umbenennt, traegt den alten NAMEN hier
284
+ ein" -- gejoint wird aber ein DATEIname. Wer `"k3alt"` eintraegt, bekam
285
+ keinen Fehler: nichts wird entfernt, nichts gemeldet.
286
+ */
287
+ const alt = eintrag.endsWith(".md") ? eintrag : `${eintrag}.md`;
288
+ const weg = join(zielOrdner, alt);
289
+ try {
290
+ if (!existsSync(weg))
291
+ continue;
292
+ const inhalt = leseWennMoeglich(weg);
293
+ if (inhalt === null || !inhalt.includes(MARKE)) {
294
+ // Nicht von uns (oder nicht lesbar) -> nicht anfassen. Still, weil es
295
+ // der Normalfall ist, dass dort gar nichts liegt.
296
+ continue;
297
+ }
298
+ rmSync(weg);
299
+ entfernt.push(alt);
300
+ console.log(` (veralteten Befehl /${alt.replace(/\.md$/, "")} entfernt)`);
301
+ }
302
+ catch {
303
+ // Bewusst still: Die Befehle sind installiert. Eine liegengebliebene
304
+ // alte Datei ist ein Schoenheitsfehler, kein Grund, den Erfolg zu
305
+ // widerrufen. (Review-Runde 2.)
306
+ }
307
+ }
308
+ return entfernt;
309
+ }