kimal3 0.1.4 → 0.1.6

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.
@@ -1,7 +1,9 @@
1
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, } 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
+ import { STEMPEL_MARKE } from "../nachricht.js";
6
+ import { eigeneVersion } from "../version.js";
5
7
  /**
6
8
  * Namen, die frueher einmal ausgeliefert wurden und heute nicht mehr.
7
9
  *
@@ -25,6 +27,62 @@ const VERALTETE_BEFEHLE = [];
25
27
  * wird sie NICHT ueberschrieben, sondern uebersprungen und gemeldet.
26
28
  */
27
29
  const MARKE = "kimal3:erzeugt";
30
+ /**
31
+ * Wohin das Skill installiert wird -- an EINER Stelle.
32
+ *
33
+ * 🔑 WARUM EXPORTIERT (Befund der vierten Review-Runde): `kimal3.ts` baute
34
+ * denselben Pfad ein zweites Mal zusammen, um dort den Stempel zu lesen. Zoege
35
+ * das Ziel je um, faende `existsSync` dort nichts, `skillZustand` antwortete
36
+ * "still" -- und Weg 2 waere lautlos tot. Kein Fehler, keine Meldung, nichts
37
+ * zum Bemerken.
38
+ *
39
+ * Dieselbe Lehre wie bei `version.ts`, nur eine Stunde spaeter: *Zwei
40
+ * Wahrheiten ueber denselben Ort driften, und die zweite verrottet, ohne
41
+ * dass jemand hinsieht.*
42
+ */
43
+ export function skillOrdner() {
44
+ return join(homedir(), ".claude", "skills", "kimal3");
45
+ }
46
+ export function skillDatei() {
47
+ return join(skillOrdner(), "SKILL.md");
48
+ }
49
+ /**
50
+ * Haengt die eigene Versionsnummer ans ENDE des Skill-Textes.
51
+ *
52
+ * 🔴 DER STEMPEL BESCHREIBT DIE SKILL.md -- NICHT DEN GANZEN LAUF.
53
+ * Diese Zuordnung hat drei Review-Runden gekostet und wurde zweimal falsch
54
+ * entschieden; sie steht hier ausfuehrlich, damit es kein viertes Mal gibt:
55
+ *
56
+ * Runde 2 verschob den Stempel ans Ende des Befehls, damit ein halber Lauf
57
+ * nicht "alles aktuell" behauptet. Runde 3 fand eine Luecke darin, Runde 4
58
+ * flickte den Rueckgabewert -- und Runde 5 zeigte, dass die ganze Richtung
59
+ * falsch war: Bei JEDEM stabilen Fehlschlag danach (fremde k3*.md im
60
+ * Ordner, schreibgeschuetzter Befehlsordner) ist die SKILL.md geschrieben,
61
+ * aber ungestempelt. `skillZustand` antwortet dann "veraltet", die CLI
62
+ * meldet TAEGLICH "deine Skilldateien sind aelter, tippe kimal3 skill
63
+ * install" -- und genau dieser Befehl stellt denselben Zustand wieder her.
64
+ *
65
+ * *`skillZustand` liest die SKILL.md. Also muss der Stempel ueber die SKILL.md
66
+ * sprechen und ueber nichts sonst.* Ist sie geschrieben, ist sie aktuell --
67
+ * unabhaengig davon, was mit den Slash-Befehlen passiert ist. Deren Zustand
68
+ * meldet der Befehl mit Text und Rueckgabewert, sofort und einmalig; das ist
69
+ * der richtige Kanal dafuer, nicht ein taeglicher Hinweis, den niemand
70
+ * abstellen kann.
71
+ *
72
+ * 🔑 Und er entsteht in DERSELBEN Schreiboperation wie die Datei. Kein zweites
73
+ * Lesen, kein zweites Schreiben, kein Zustand dazwischen, in dem die Datei
74
+ * ohne ihren Stempel dasteht.
75
+ *
76
+ * ⚠️ Ohne eigene Nummer GAR KEIN Stempel. Ein erfundener Ersatzwert waere
77
+ * schlimmer als keiner: Die CLI vergliche spaeter gegen ihre eigene Erfindung.
78
+ * Ohne Stempel antwortet `skillZustand` "veraltet" -- die harmlose Seite.
79
+ */
80
+ function mitStempel(inhalt) {
81
+ const eigene = eigeneVersion();
82
+ if (eigene === null)
83
+ return inhalt;
84
+ return `${inhalt.trimEnd()}\n\n<!-- ${STEMPEL_MARKE}${eigene} -->\n`;
85
+ }
28
86
  /**
29
87
  * Legt das Skill UND die Slash-Befehle GLOBAL ab, damit Claude dieses Werkzeug
30
88
  * ueberall kennt -- und damit der Mensch es auch selbst ausloesen kann.
@@ -63,14 +121,46 @@ export async function skillInstall() {
63
121
  // Erfolgsprofil ein" gilt in JEDEM Ordner, nicht in einem. Wer projektlokal
64
122
  // installiert, hat das Werkzeug im naechsten Projekt nicht -- und merkt es
65
123
  // erst, wenn er dort vergeblich "fertig" sagt.
66
- const zielOrdner = join(homedir(), ".claude", "skills", "kimal3");
67
- const ziel = join(zielOrdner, "SKILL.md");
124
+ const zielOrdner = skillOrdner();
125
+ const ziel = skillDatei();
68
126
  // Vorher merken: "angelegt" und "aktualisiert" sind zwei verschiedene
69
127
  // Wirklichkeiten und bekommen deshalb zwei verschiedene Meldungen.
70
128
  const gabEsSchon = existsSync(ziel);
71
129
  try {
72
130
  mkdirSync(zielOrdner, { recursive: true });
73
- copyFileSync(quelle, ziel);
131
+ /*
132
+ 🔴 DER STEMPEL GEHOERT ANS ENDE, NIEMALS AN DEN ANFANG. SKILL.md beginnt
133
+ in Zeile 1 mit YAML-Frontmatter (`---`, dann `name:`). Steht davor
134
+ irgendetwas, ist es kein Frontmatter mehr -- der Skill wird dann gar
135
+ nicht geladen, und zwar OHNE Fehlermeldung. Ein Update-Hinweis, der den
136
+ Skill abschaltet, den er aktuell halten soll, ist der teuerste denkbare
137
+ Fehler in dieser Scheibe.
138
+
139
+ Deshalb auch lesen-anhaengen-schreiben statt copyFileSync: Der Stempel
140
+ entsteht beim KOPIEREN, er steht nicht in der Quelle -- sonst truege das
141
+ Paket seine Versionsnummer an zwei Orten, und die zweite verrottet.
142
+
143
+ 🔴 OHNE eigene Versionsnummer wird GAR KEIN Stempel geschrieben, sondern
144
+ unveraendert kopiert. `eigeneVersion()` liefert `null`, wenn
145
+ `cli/package.json` fehlt oder Unsinn enthaelt. Ein Stempel mit einem
146
+ erfundenen Ersatzwert waere hier schlimmer als keiner: Die CLI vergliche
147
+ spaeter gegen ihre eigene Erfindung und meldete entweder ein Update, das
148
+ es nicht gibt, oder schwiege zu einem, das faellig ist. Ohne Stempel
149
+ antwortet `skillZustand` dagegen "veraltet" -- die harmlose Seite, denn
150
+ ein zweites `kimal3 skill install` kostet nichts.
151
+
152
+ ⚠️ Die Marke `kimal3:erzeugt` (Konstante MARKE oben) steckt in
153
+ `STEMPEL_MARKE` und bleibt damit erhalten. Der Stempel ist eine
154
+ ERWEITERUNG dieser Marke, kein Ersatz -- `raeumeVeralteteAuf` und die
155
+ Ueberschreib-Pruefung erkennen unsere eigenen Dateien genau daran.
156
+
157
+ 📌 Er entsteht HIER, in derselben Schreiboperation wie die Datei -- und
158
+ zwar wieder, nachdem er drei Runden lang am Ende des Befehls stand.
159
+ Warum die Ruecknahme richtig ist, steht ausfuehrlich bei `mitStempel`:
160
+ Der Stempel spricht ueber die SKILL.md, nicht ueber den ganzen Lauf.
161
+ */
162
+ const inhalt = readFileSync(quelle, "utf8");
163
+ writeFileSync(ziel, mitStempel(inhalt), "utf8");
74
164
  }
75
165
  catch (fehler) {
76
166
  console.error(`Das Skill liess sich nicht schreiben: ${String(fehler)}`);
@@ -142,6 +232,22 @@ export async function skillInstall() {
142
232
  console.error(" Die oben genannten stehen; ein zweiter Lauf holt den Rest.");
143
233
  return 3;
144
234
  }
235
+ /*
236
+ 🔴 Eine nicht lesbare Datei ist ein UNVOLLSTAENDIGER Lauf und bekommt
237
+ deshalb `3`, genau wie `gestoert` (Befund der vierten Review-Runde).
238
+ `3` heisst laut SKILL.md "einmal wiederholen", und das passt zur Lage:
239
+ Eine Datei in Benutzung ist gleich wieder frei.
240
+
241
+ 📌 Der STEMPEL haengt hier nicht mehr dran -- er entsteht beim Schreiben
242
+ der SKILL.md und beschreibt nur sie (ausfuehrliche Begruendung bei
243
+ `mitStempel` oben). Bis zur fuenften Review-Runde stand er an dieser
244
+ Stelle, und jeder stabile Fehlschlag danach erzeugte einen taeglichen
245
+ Hinweis, den kein Befolgen aufloesen konnte.
246
+ */
247
+ if (unlesbar.length > 0) {
248
+ console.error(" Der Lauf ist damit unvollstaendig -- ein zweiter Versuch holt den Rest.");
249
+ return 3;
250
+ }
145
251
  /*
146
252
  ⚠️ **Nicht auf einen Befehl verweisen, den wir gerade NICHT installiert
147
253
  haben** (Review-Runde 3, 2026-08-18, gemessen). Mit einer fremden
@@ -0,0 +1,389 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { dateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
4
+ import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
5
+ /**
6
+ * Legt **nur eine Werkbank** an — in einem Firmen-Ordner, der schon steht.
7
+ *
8
+ * 🔴 **Warum es diesen Befehl gibt** (gefunden von Dennis am 2026-08-20, im
9
+ * Rollenspiel als Angestellter einer Kundenfirma — nicht von einem Prüfer, und
10
+ * nicht in fünf Review-Runden). Gemessen:
11
+ *
12
+ * Im bestehenden Firmen-Ordner nochmal `kimal3 arbeitsplatz neu`
13
+ * → „Der Ordner gibt es schon und ist nicht leer. Gib einen anderen
14
+ * Namen an."
15
+ * Also gibt er einen an: `kimal3 arbeitsplatz neu werkbank-sabine`
16
+ * → ✓ 22 Dateien, darunter `werkbank-sabine/kontext/`
17
+ *
18
+ * Er bekommt **keinen Fehler**, sondern eine ZWEITE Firma im Bauch der ersten:
19
+ * eigene `firma.md`, eigene `marke.md`, eigene `sicherheit.md`. Und der
20
+ * Fehlertext des Befehls **schlägt diesen Weg selbst vor**.
21
+ *
22
+ * 🔑 **Die Vorlage wusste längst, dass es anders sein sollte.** Die
23
+ * Firmen-`.gitignore` erklärt in ihrem eigenen Kommentar die Ordner
24
+ * `werkbank-<name>` als Geschwister neben `kontext/`. *Der Entwurf sah eine
25
+ * Form vor, die die CLI nicht herstellen konnte.*
26
+ *
27
+ * arbeitsplatz neu → einmal pro FIRMA (die ganze Familie)
28
+ * werkbank neu → einmal pro MENSCH (nur der eigene Teil)
29
+ *
30
+ * 🔴 **GRENZE, die dieser Befehl heute NICHT aufhebt** (Review-Runde 4 zu
31
+ * PR #25, nachgemessen an `src/lib/pro/zugang.ts`): Er holt die Vorlage über
32
+ * `/api/cli/arbeitsplatz`, und die Tür dort fragt `istPro(user_id)` — eine
33
+ * Zeile in `konto_plan`, **pro Person**. Es gibt keinen Weg über
34
+ * `memberships`/`organizations`.
35
+ *
36
+ * Der zweite Mensch einer Kundenfirma — also genau der, für den dieser Befehl
37
+ * gebaut wurde — braucht damit ein **eigenes Pro-Konto**. Hat er keins,
38
+ * bekommt er `402` und Rückgabewert `4`, und `SKILL.md` weist Claude dort
39
+ * ausdrücklich an, NICHT zu wiederholen.
40
+ *
41
+ * ⚠️ Das ist **keine Panne, sondern eine offene Produktfrage**: Erbt ein
42
+ * Firmenmitglied den Zugang, oder kauft jeder einzeln? Sie gehört Dennis, nicht
43
+ * diesem Befehl — deshalb steht sie in `WEITER-HIER.md` unter „Entscheidungen"
44
+ * und nicht als stille Erweiterung von `istPro` hier drin.
45
+ * *Eine Grenze, die man benennt, ist etwas anderes als eine, die man
46
+ * übersieht* — und sie reiht sich neben die schon bekannte: Der Angestellte hat
47
+ * ohnehin noch keinen gebauten Weg an den Firmen-Ordner (Stufe 2).
48
+ *
49
+ * 🔴 **Die wichtigste Eigenschaft: Er fasst die Firmenebene NICHT an.** Kein
50
+ * einziges Byte in `kontext/`, `CLAUDE.md`, `README.md`, `.gitignore` — auch
51
+ * nicht schreibend „identisch drüber". Das ist keine Höflichkeit, sondern
52
+ * Vorbereitung: Perspektivisch soll die Firma ihre `kontext/`-Ebene zentral
53
+ * verwalten (s. `WEITER-HIER.md`, Entscheidungen). Was dann von außen kommt,
54
+ * darf ein lokaler Befehl nicht überschreiben.
55
+ */
56
+ const HILFE = `
57
+ kimal3 werkbank neu [dein-vorname]
58
+
59
+ Legt eine eigene Werkbank in einem bestehenden Firmen-Ordner an.
60
+ Die Firmenebene (kontext/, CLAUDE.md, README.md) wird NICHT angefasst.
61
+
62
+ dein-vorname Optional. Der Ordner traegt deinen Namen -- aus "sabine"
63
+ wird "werkbank-sabine". Ohne Angabe steht dort der Name,
64
+ den der Server aus deiner Anmelde-Adresse gebildet hat.
65
+
66
+ 🔴 Bricht ab, wenn der Zielordner schon existiert und NICHT leer ist.
67
+
68
+ Du hast noch gar keinen Firmen-Ordner?
69
+ kimal3 arbeitsplatz neu <dein-vorname>
70
+ `;
71
+ /**
72
+ * 🔴 **Review-Runde 2 zu PR #25 — und es ist DERSELBE Fehler wie in Runde 1,
73
+ * nur eine Datei weiter.** Hier stand:
74
+ *
75
+ * Gib einen anderen Namen an:
76
+ * kimal3 werkbank neu <ein-anderer-vorname>
77
+ *
78
+ * Der haeufigste Grund fuer diese Absage ist, dass der Mensch seine Werkbank
79
+ * **schon angelegt hat**. Befolgt er den Rat, bekommt er eine ZWEITE
80
+ * persoenliche Werkbank neben der ersten (`werkbank-sabine-2`) — dieselbe
81
+ * stille Verdopplung, die dieser PR bei `arbeitsplatz neu` gerade beseitigt
82
+ * hat.
83
+ *
84
+ * *Ich habe den Fehler bei einem Befehl behoben und ihn beim Zwilling stehen
85
+ * lassen — obwohl die Behebung des einen der Anlass fuer den anderen war.*
86
+ *
87
+ * Der Rat nennt jetzt zuerst den wahrscheinlichen Fall (du bist es selbst) und
88
+ * dann den seltenen (du bist es nicht).
89
+ *
90
+ * ⚠️ Und er ist eine FUNKTION, keine Konstante -- aus demselben Grund wie beim
91
+ * Zwilling in `arbeitsplatz.ts`: Der Befehl kennt den Ordner, in den er schickt.
92
+ * Ein Platzhalter wie `cd <der-ordner-oben>` waere eine Luecke, die der Mensch
93
+ * fuellen muss, obwohl die Antwort danebensteht. *Ein Platzhalter ist ein
94
+ * Eingestaendnis, dass man es nicht weiss -- hier weiss man es.*
95
+ */
96
+ function hinweisFuer(ordnerName) {
97
+ const ziel = ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName;
98
+ return (" Ist das DEINE Werkbank? Dann steht sie schon -- geh einfach hinein:\n" +
99
+ ` cd ${ziel}\n` +
100
+ " Bist du es nicht, nimm deinen eigenen Vornamen:\n" +
101
+ " kimal3 werkbank neu <dein-vorname>");
102
+ }
103
+ export async function werkbankNeu(argv) {
104
+ if (argv.includes("--help") || argv.includes("-h")) {
105
+ console.log(HILFE);
106
+ return 0;
107
+ }
108
+ /*
109
+ Dieselben drei Absagen wie bei `arbeitsplatz neu` -- bewusst wortgleich: Wer
110
+ den einen Befehl kennt, soll beim anderen nichts Neues lernen muessen.
111
+
112
+ ⚠️ Sie sprechen seit dem 2026-08-20 von einem NAMEN statt von einem
113
+ Ordnernamen. Die alte Hilfe hier hat ausdruecklich
114
+ `kimal3 werkbank neu werkbank-sabine` beigebracht -- deshalb schneidet
115
+ `werkbankOrdnerAus` ein bereits mitgetipptes `werkbank-` wieder ab, statt
116
+ `werkbank-werkbank-sabine` zu bauen. *Alte Anleitungen ueberleben laenger
117
+ als der Code, den sie beschreiben.*
118
+ */
119
+ const nameArg = argv[0];
120
+ if (nameArg !== undefined && nameArg.trim() === "") {
121
+ console.error(`Ein leerer Name ist kein gueltiger Name.\n${HILFE}`);
122
+ return 1;
123
+ }
124
+ if (nameArg !== undefined && nameArg.startsWith("--")) {
125
+ console.error(`Die Option „${nameArg}" kenne ich nicht.\n${HILFE}`);
126
+ return 1;
127
+ }
128
+ if (argv.length > 1) {
129
+ /*
130
+ 🔴 **Der Vorschlag darf keine Optionen einsammeln** (Review-Runde 5 zu
131
+ PR #25). Hier stand `argv.join("-")` -- aus `werkbank neu sabine --force`
132
+ wurde damit der Rat `werkbank neu sabine---force`. Die `--`-Wache ein paar
133
+ Zeilen hoeher sieht nur `argv[0]`.
134
+ *Ein Rat, der ein falsches Kommando erzeugt, ist schlimmer als keiner* --
135
+ dieselbe Klasse wie die Absagen aus Runde 1 und 2, nur kleiner.
136
+
137
+ Deshalb: Sieht irgendein Wort wie eine Option aus, gibt es gar keinen
138
+ Vorschlag. Ein Mensch, der `--force` tippt, sucht keine Schreibhilfe fuer
139
+ seinen Vornamen.
140
+ */
141
+ const woerter = argv.filter((w) => !w.startsWith("-"));
142
+ const vorschlag = woerter.length === argv.length && woerter.length > 1
143
+ ? `Besteht dein Name aus mehreren Woertern? Dann schreib ihn zusammen:\n` +
144
+ ` kimal3 werkbank neu ${woerter.join("-")}\n`
145
+ : "";
146
+ console.error(`Mit „${argv[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
147
+ `${vorschlag}${HILFE}`);
148
+ return 1;
149
+ }
150
+ // `null` heisst: nach dem Umschreiben blieb kein Zeichen uebrig, das in einem
151
+ // Ordnernamen stehen darf. Ein Mensch hat hier getippt und verdient eine
152
+ // Antwort -- der Server schweigt im selben Fall und bleibt beim Vordruck.
153
+ let gewuenschteWerkbank = null;
154
+ if (nameArg !== undefined) {
155
+ gewuenschteWerkbank = werkbankOrdnerAus(nameArg);
156
+ if (gewuenschteWerkbank === null) {
157
+ console.error(`Aus „${nameArg}" kann ich keinen Ordnernamen machen -- es bleiben nur`);
158
+ console.error("Zeichen uebrig, die in keinem Ordnernamen stehen duerfen.");
159
+ console.error("");
160
+ console.error(" Nimm deinen Vornamen: kimal3 werkbank neu sabine");
161
+ return 1;
162
+ }
163
+ }
164
+ const holen = await vorlageHolen();
165
+ if (!holen.ok)
166
+ return holen.wert;
167
+ const daten = holen.daten;
168
+ const werkbank = werkbankOrdner(daten.dateien);
169
+ /*
170
+ 🔴 **Review-Runde 1 zu PR #25: eine Asymmetrie zur Schwesterdatei.**
171
+ `arbeitsplatz.ts` prueft `daten.wurzel` ausdruecklich mit `istUnsichererPfad`
172
+ („kommt genauso ROH vom Server wie jeder `datei.pfad` -- dieselbe Bremse").
173
+ Hier wurde `werkbank` genauso roh benutzt, nur ungeprueft.
174
+
175
+ Der Fall: Eine manipulierte Antwort (z. B. ueber ein umgebogenes
176
+ `KIMAL3_URL`) enthaelt den Pfad `../CLAUDE.md`. `werkbankAusDateien` zerlegt
177
+ ihn in zwei Teile und liefert `".."` -- und `dateienSchreiben` macht daraus
178
+ `resolve("..")`, also den ELTERNORDNER.
179
+
180
+ ⚠️ Und die Firmen-Wache faengt das nicht: `firmenEbeneAus` gibt fuer so eine
181
+ Antwort eine leere Liste zurueck, `fehlend.length > 0` ist damit falsch, und
182
+ die Pruefung geht still durch.
183
+
184
+ 🔬 **Ehrlich nachgemessen: fuer `..` haette er trotzdem nichts geschrieben.**
185
+ Der Angriff wurde mit einem eigenen Server nachgestellt (Antwort mit
186
+ `../CLAUDE.md`), einmal mit und einmal ohne diese Bremse. Ohne sie faengt ihn
187
+ der Nicht-leer-Riegel in `dateienSchreiben`: Ein Elternordner enthaelt immer
188
+ mindestens den Ordner, in dem man steht, ist also nie leer.
189
+
190
+ 🔴 **Fuer `""` stimmte das NICHT -- und genau das stand hier als Begruendung**
191
+ (Review-Runde 2). Der Satz lautete, `""` fuehre auf das aktuelle Verzeichnis,
192
+ „das `kontext/` enthaelt, sonst waere die Wache oben schon rot". Ist sie
193
+ aber nicht: Bei einer Antwort aus `/CLAUDE.md` liefert `werkbankAusDateien`
194
+ den leeren Teil `""`, und `firmenEbeneAus` UEBERSPRINGT genau diese Pfade
195
+ (`teile[0] === werkbank`) -- die Firmen-Wache sieht `[]` und laesst durch.
196
+ In einem leeren Ordner waeren die Dateien dann dorthin gelandet, wo der
197
+ Mensch steht.
198
+ *Ein Kommentar, der eine Luecke mit einer anderen Wache begruendet, muss
199
+ diese Wache im FEHLERFALL gelesen haben, nicht im Normalfall.*
200
+ Deshalb prueft hier jetzt `istUnsichererOrdnername` statt
201
+ `istUnsichererPfad`: Ein Ordnername ist EIN Pfadteil, und der ist weder leer
202
+ noch `.` noch `..`.
203
+
204
+ 🔑 **Der Gewinn ist deshalb die MELDUNG, nicht die Abwehr.** Ohne diese
205
+ Zeile las der Mensch *„Der Ordner `..` gibt es schon und ist nicht leer. Gib
206
+ einen anderen Namen an"* -- und sucht den Fehler bei sich, obwohl der Server
207
+ luegt. Jetzt steht dort `Bitte melden`, und der Rueckgabewert ist `3` statt
208
+ `1`, also „Stoerung" statt „deine Eingabe passt nicht". *Kein Fehlerfall
209
+ traegt den Namen eines anderen* -- und dass die Abwehr schon anderswo steht,
210
+ ist kein Grund, sie hier falsch zu benennen.
211
+ */
212
+ if (werkbank !== null && istUnsichererOrdnername(werkbank)) {
213
+ console.error(`Die Antwort enthaelt einen unsicheren Ordnernamen: „${werkbank}". Bitte melden.`);
214
+ return 3;
215
+ }
216
+ if (werkbank === null) {
217
+ console.error("In der Vorlage steckt kein Werkbank-Ordner -- damit kann ich hier nichts anlegen.");
218
+ console.error("Bitte melden.");
219
+ return 3;
220
+ }
221
+ /*
222
+ 🔑 **Die Prüfung, ob wir überhaupt in einer Firma stehen** -- und sie ist
223
+ keine Förmlichkeit: In `<werkbank>/.claude/settings.json` steht
224
+ `"additionalDirectories": ["../kontext"]`. Eine Werkbank ohne Firmenebene
225
+ daneben zeigt damit ins Leere, und Claude fände den Firmenkontext nie.
226
+ Das merkt man erst Tage später -- genau die Sorte Fehler, gegen die dieser
227
+ Befehl gebaut wurde.
228
+
229
+ ⚠️ Welche Ordner das sind, wird ABGELEITET und nicht getippt (dieselbe Regel
230
+ wie bei `werkbankOrdner`): Firmenebene ist alles, was in der Lieferung
231
+ NEBEN dem Werkbank-Ordner in einem eigenen Unterordner liegt. Heute ist das
232
+ `kontext`. Wird die Vorlage umgebaut, wandert diese Prüfung mit -- statt
233
+ auf einen Ordnernamen zu zeigen, den es nicht mehr gibt.
234
+ */
235
+ const firmenOrdner = firmenEbeneAus(daten.dateien, werkbank);
236
+ const fehlend = firmenOrdner.filter((o) => !istOrdner(o));
237
+ /*
238
+ 🔴 **Die leere Liste ist ein eigener Fall** (Review-Runde 3 zu PR #25).
239
+ Vorher stand hier nur `fehlend.length > 0` -- und eine LEERE Liste hat
240
+ nichts, was fehlen koennte. Liefert die Antwort keine Firmenebene, ging die
241
+ Wache still durch und legte die Werkbank in irgendein Verzeichnis, in dem
242
+ der Mensch gerade steht. Ihr `../kontext` zeigte dann ins Leere.
243
+ *Eine Wache, die aus einer leeren Liste „alles in Ordnung" liest, hat die
244
+ Frage nicht gestellt, fuer die sie gebaut wurde.*
245
+
246
+ Heute nicht ausloesbar (`check:arbeitsplatz` Pruefung 2 erzwingt `kontext/*`)
247
+ -- erreichbar also nur ueber eine kaputte oder manipulierte Antwort.
248
+ */
249
+ /*
250
+ 🔴 **ZWEI Wirklichkeiten, zwei Ausgaenge** (Review-Runde 4 zu PR #25, und der
251
+ Fehler stammt aus dem Fix von Runde 3).
252
+
253
+ Runde 3 hat `firmenOrdner.length === 0` mit in dieselbe Bedingung gehaengt.
254
+ Dieser Zweig hat per Definition NICHTS in `fehlend` -- `join` lieferte `""`,
255
+ und der Mensch las woertlich *„Hier steht kein Firmen-Ordner -- ich sehe
256
+ kein ."* Beim Reparieren fiel der zweite Teil auf: Danach folgten
257
+ unveraendert die Raete fuer jemanden, der am FALSCHEN ORT steht („Geh
258
+ dorthin", „Bist du der ERSTE in deiner Firma?"). Die helfen nicht, wenn der
259
+ SERVER kaputt ist.
260
+
261
+ *Ein Fehlerfall darf nie denselben Namen tragen wie ein anderer* -- und der
262
+ Rueckgabewert ist Teil des Namens: `3` heisst laut `cli/skill/SKILL.md`
263
+ „Stoerung, gleich noch einmal", `1` heisst „deine Eingabe passt nicht".
264
+ Eine kaputte Lieferung ist keine falsche Eingabe.
265
+ */
266
+ if (firmenOrdner.length === 0) {
267
+ console.error("Die Vorlage vom Server nennt gar keine Firmenebene -- damit kann ich");
268
+ console.error("nicht pruefen, ob du an der richtigen Stelle stehst.");
269
+ console.error("Das liegt nicht an dir. Bitte melden.");
270
+ return 3;
271
+ }
272
+ if (fehlend.length > 0) {
273
+ console.error("Hier steht kein Firmen-Ordner -- ich sehe kein " + fehlend.join(", ") + ".");
274
+ console.error("Eine Werkbank braucht die Firmenebene daneben; ohne sie findet Claude");
275
+ console.error("euren Firmenkontext nicht.");
276
+ console.error("");
277
+ console.error(" Steht er woanders? Geh dorthin und versuch es dort noch einmal.");
278
+ console.error("");
279
+ /*
280
+ 🔴 **Hier stand „Du hast noch keinen? kimal3 arbeitsplatz neu"**
281
+ (Befund Dennis, 2026-08-20, wenige Minuten nach dem Bauen). Das ist
282
+ derselbe falsche Rat wie der aus `SKILL.md`, nur eine Ebene hoeher:
283
+
284
+ Fuer den ERSTEN Menschen einer Firma stimmt er. Fuer den ZWEITEN --
285
+ also genau den, fuer den dieser Befehl gebaut wurde -- legt er eine
286
+ leere Zweitfirma an, statt ihn an die echte zu bringen. Und diese
287
+ zweite Firma ist schlimmer als die verschachtelte von vorhin: Sie
288
+ sieht voellig richtig aus.
289
+
290
+ *Ich habe den Fehler aus SKILL.md entfernt und ihn in meiner eigenen
291
+ Absage wieder eingebaut. Ein Rat, der fuer die Haelfte der Leser
292
+ falsch ist, muss die Haelften benennen.*
293
+
294
+ ⚠️ Der zweite Fall hat heute KEINE gute Antwort -- der Angestellte hat
295
+ keinen gebauten Weg an den Firmen-Ordner seiner Firma (s. WEITER-HIER,
296
+ "Verwaltet die Firma ihre kontext/-Ebene kuenftig in der App?"). Solange
297
+ das so ist, ist die ehrliche Auskunft „frag, wo er liegt" -- und nicht
298
+ ein Befehl, der etwas Falsches anlegt.
299
+ */
300
+ console.error(" Bist du der ERSTE in deiner Firma?");
301
+ console.error(" kimal3 arbeitsplatz neu <dein-vorname>");
302
+ console.error("");
303
+ console.error(" Hat deine Firma schon einen? Dann hol dir DEN -- z. B. als Kopie");
304
+ console.error(" oder ueber euer Repo. Leg keinen neuen an, sonst habt ihr zwei");
305
+ console.error(" verschiedene Firmenordner mit verschiedenen Regeln darin.");
306
+ return 1;
307
+ }
308
+ /*
309
+ 🔑 Der Zielordner ist der Werkbank-Name -- entweder der, den der Server
310
+ schon gesetzt hat (`werkbank`, aus der Anmelde-Adresse gebildet), oder der
311
+ mitgegebene. Der Ordner TRAEGT damit immer einen Namen, und genau darum ging
312
+ es bei der Umbenennung: *Wo ein Name im Ordner steht, gehoert er einer
313
+ Person; wo keiner steht, gehoert er allen.*
314
+ */
315
+ const ordnerName = gewuenschteWerkbank ?? werkbank;
316
+ /*
317
+ Erst UMBENENNEN, dann ausschneiden -- und zwar auch im Inhalt.
318
+
319
+ ⚠️ **Hier stand: „Die Dateien muessen dafuer nicht umgeschrieben werden."**
320
+ Der Satz war wahr, als ich ihn schrieb: Heute nennt keine Datei unterhalb der
321
+ Werkbank ihren eigenen Ordner (nachgemessen), und die Texte, die ihn nennen,
322
+ gehoeren zur Firmenebene, die dieser Befehl nicht anfasst.
323
+
324
+ Nur HAELT das nichts. Wer morgen in die Werkbank-`CLAUDE.md` schreibt „deine
325
+ Projekte liegen in `werkbank-sabine/projekte/`", baut damit einen Satz ein,
326
+ der beim naechsten Menschen auf den falschen Ordner zeigt -- ohne dass etwas
327
+ fehlschlaegt. *Die Prueffrage aus CLAUDE.md: Beschreibt der Kommentar ein
328
+ Verhalten, das jemand WILL -- oder nur eines, das PASSIERT?* Hier passierte
329
+ es. Also nehmen wir die Annahme heraus, statt eine Wache um sie zu bauen.
330
+
331
+ Die Reihenfolge ist der ganze Trick: Nach dem Umbenennen heisst der Praefix
332
+ schon wie das Ziel, und der Ausschnitt schneidet ihn mit derselben
333
+ Zeichenkette weg, die vorher eingesetzt wurde. Aus `werkbank-jonas/CLAUDE.md`
334
+ wird `CLAUDE.md`, das Ziel ist der neue Ordner selbst.
335
+
336
+ 🔑 Das ist die einzige Stelle, an der sich dieser Befehl vom grossen
337
+ unterscheidet -- alles andere (Ueberschreib-Schutz, Pfad-Pruefung, Fehler-
338
+ Trennung) ist dieselbe Maschinerie aus `arbeitsplatz-dateien.ts`.
339
+ */
340
+ const umbenannt = werkbankUmbenennen(daten.dateien, werkbank, ordnerName);
341
+ const praefix = `${ordnerName}/`;
342
+ const meine = umbenannt
343
+ .filter((d) => d.pfad.startsWith(praefix))
344
+ .map((d) => ({ pfad: d.pfad.slice(praefix.length), inhalt: d.inhalt }));
345
+ const wert = dateienSchreiben(meine, ordnerName, hinweisFuer(ordnerName));
346
+ if (wert !== 0)
347
+ return wert;
348
+ console.log(`✓ ${meine.length} ${meine.length === 1 ? "Datei" : "Dateien"} angelegt.`);
349
+ console.log(` ${resolve(ordnerName)}`);
350
+ console.log("");
351
+ console.log(" Die Firmenebene ist unberuehrt geblieben.");
352
+ console.log("");
353
+ console.log(" Weiter geht's:");
354
+ console.log(` cd ${ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName}`);
355
+ console.log(" claude");
356
+ console.log(' dann "Lass uns einrichten" tippen');
357
+ return 0;
358
+ }
359
+ /**
360
+ * Die Ordner der Firmenebene, aus der Lieferung abgeleitet: jeder erste
361
+ * Pfad-Teil, der einen Unterordner benennt und nicht die Werkbank ist.
362
+ *
363
+ * Dateien direkt an der Wurzel (`CLAUDE.md`, `README.md`, `.gitignore`) zaehlen
364
+ * bewusst NICHT mit: Sie sagen weniger verlaesslich, dass hier eine Firma steht
365
+ * -- eine `CLAUDE.md` liegt in fast jedem Projektordner.
366
+ */
367
+ function firmenEbeneAus(dateien, werkbank) {
368
+ const ordner = new Set();
369
+ for (const datei of dateien) {
370
+ const teile = datei.pfad.split("/");
371
+ if (teile.length < 2)
372
+ continue;
373
+ if (teile[0] === werkbank)
374
+ continue;
375
+ ordner.add(teile[0]);
376
+ }
377
+ return [...ordner].sort();
378
+ }
379
+ function istOrdner(name) {
380
+ try {
381
+ return existsSync(name) && statSync(name).isDirectory();
382
+ }
383
+ catch {
384
+ // Nicht lesbar heisst "kann ich nicht bestaetigen" -- und was ich nicht
385
+ // bestaetigen kann, behandle ich als fehlend. Lieber eine Absage zu viel
386
+ // als eine Werkbank, die ins Leere zeigt.
387
+ return false;
388
+ }
389
+ }
@@ -47,6 +47,43 @@ export const BEFEHLE = [
47
47
  "Geklappt hat es, wenn er „Skill angelegt“ meldet und die Slash-Befehle aufzählt. Beides kennt Claude ab der NÄCHSTEN Sitzung — die laufende einmal neu starten.",
48
48
  ],
49
49
  },
50
+ {
51
+ befehl: "kimal3 arbeitsplatz neu",
52
+ beschreibung: "Den ganzen KI-Arbeitsplatz anlegen — Firmenebene und Werkbank",
53
+ wer: "du",
54
+ rhythmus: "einmal pro Firma",
55
+ tippen: "! kimal3 arbeitsplatz neu",
56
+ terminal: "kimal3 arbeitsplatz neu",
57
+ details: [
58
+ "Holt die Vorlage von deinem Pro-Konto und legt sie hier als echten Ordner an — CLAUDE.md, Takt-Befehle, die Werkbank für deine Projekte. Dasselbe Paket, das auch als Download auf /pro liegt.",
59
+ "Der Ordner heißt immer KI-Arbeitsplatz — mehrere Firmen trennst du über den Ordner darüber, z. B. Kunden/ARNO/KI-Arbeitsplatz. Gib deinen Vornamen mit, dann heißt deine Werkbank darin werkbank-sabine: kimal3 arbeitsplatz neu sabine.",
60
+ "Gibt es den Zielordner schon und ist er nicht leer, bricht der Befehl ab, statt etwas zu überschreiben — deine Arbeit ist sicher. Geklappt hat es, wenn er die Zahl der angelegten Dateien nennt.",
61
+ "⚠️ Er legt auch die Firmenebene an (kontext/ mit Marke, Systemen, Sicherheit). Steht die schon, brauchst du kimal3 werkbank neu — sonst entsteht eine zweite Firma in der ersten.",
62
+ ],
63
+ },
64
+ /*
65
+ 🔴 Der Eintrag steht direkt NEBEN `arbeitsplatz neu` und nicht am Ende der
66
+ Liste (die Reihenfolge ist laut Kopf dieser Datei „der Weg, den ein Mensch
67
+ geht"). Genau hier steht der Mensch vor der Verwechslung: Beide legen
68
+ Ordner aus derselben Vorlage an, und die falsche Wahl merkt man erst Tage
69
+ spaeter. Nebeneinander sind die verschiedenen `rhythmus`-Angaben --
70
+ „einmal pro Firma" gegen „einmal pro Mensch" -- die kuerzeste Erklaerung
71
+ des Unterschieds, die es gibt.
72
+ */
73
+ {
74
+ befehl: "kimal3 werkbank neu",
75
+ beschreibung: "Nur eine eigene Werkbank in einer bestehenden Firma anlegen",
76
+ wer: "du",
77
+ rhythmus: "einmal pro Mensch",
78
+ tippen: "! kimal3 werkbank neu",
79
+ terminal: "kimal3 werkbank neu",
80
+ details: [
81
+ "Für alle außer der ersten Person: Der Firmen-Ordner steht schon (kontext/ mit Marke, Systemen, Sicherheit) — du brauchst nur deinen eigenen Arbeitsplatz darin.",
82
+ "🔴 Er fasst die Firmenebene nicht an. Kein Byte in kontext/, CLAUDE.md oder README.md — auch nicht schreibend „identisch drüber“.",
83
+ "Er läuft nur, wenn du wirklich in einem Firmen-Ordner stehst. Fehlt die Firmenebene daneben, bricht er ab und sagt es — eine Werkbank ohne sie zeigt ins Leere, weil ihre Einstellungen auf ../kontext verweisen.",
84
+ "Gib deinen Vornamen mit — der Ordner trägt ihn: aus sabine wird werkbank-sabine. Ohne Angabe bildet der Server ihn aus deiner Anmelde-Adresse. So sieht man später, wem welche Werkbank gehört.",
85
+ ],
86
+ },
50
87
  {
51
88
  befehl: "kimal3 projekt neu …",
52
89
  beschreibung: "Ein Projekt ins Erfolgsprofil eintragen",
@@ -118,3 +155,25 @@ ${zeilen}
118
155
  Mehr zu einem Befehl: kimal3 projekt neu --help
119
156
  `;
120
157
  }
158
+ /**
159
+ * Wie ein Befehl **getippt** wird — aus dem Katalog geholt, nicht abgeschrieben.
160
+ *
161
+ * 🔑 **Warum das eine eigene Funktion ist** (2026-08-20): Auf `/pro` stand
162
+ * `const ANMELDEN = "! kimal3 login"` als Zeichenkette im Seitencode — eine
163
+ * zweite Wahrheit ueber einen Befehl, den der Katalog schon fuehrt. Wer im
164
+ * Katalog umbenennt, laesst sie stehen, und die Seite zeigt weiter den alten
165
+ * Aufruf. Dieselbe Klasse Fehler wie der fest getippte `cd`-Pfad in Runde 3.
166
+ *
167
+ * ⚠️ Sie **wirft**, statt einen leeren Text zu liefern. Die Aufrufer sind
168
+ * Modul-Konstanten — ein falscher Name fliegt damit beim `npm run build` auf,
169
+ * nicht erst beim Kunden vor einem Kopier-Kasten, in dem nichts steht.
170
+ * *Ein stiller Fehler auf einer bezahlten Seite ist teurer als ein lauter im
171
+ * Build.*
172
+ */
173
+ export function tippBefehl(befehl) {
174
+ const eintrag = BEFEHLE.find((b) => b.befehl === befehl);
175
+ if (!eintrag) {
176
+ throw new Error(`Kein Katalog-Eintrag fuer „${befehl}". Steht der Befehl noch in BEFEHLE?`);
177
+ }
178
+ return eintrag.tippen;
179
+ }