kimal3 0.1.5 → 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.
@@ -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
+ }
@@ -49,15 +49,39 @@ export const BEFEHLE = [
49
49
  },
50
50
  {
51
51
  befehl: "kimal3 arbeitsplatz neu",
52
- beschreibung: "Deine KI-Arbeitsplatz-Vorlage als Ordner anlegen",
52
+ beschreibung: "Den ganzen KI-Arbeitsplatz anlegen — Firmenebene und Werkbank",
53
53
  wer: "du",
54
- rhythmus: "einmal pro Arbeitsplatz",
54
+ rhythmus: "einmal pro Firma",
55
55
  tippen: "! kimal3 arbeitsplatz neu",
56
56
  terminal: "kimal3 arbeitsplatz neu",
57
57
  details: [
58
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
- "Ohne Angabe heißt der Ordner wie in der Vorlage. Brauchst du mehrere (z. B. je Firma), gib einen eigenen Namen mit: kimal3 arbeitsplatz neu <name>.",
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
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.",
61
85
  ],
62
86
  },
63
87
  {
@@ -131,3 +155,25 @@ ${zeilen}
131
155
  Mehr zu einem Befehl: kimal3 projekt neu --help
132
156
  `;
133
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
+ }
@@ -0,0 +1,85 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Welche Nachricht an welchem Tag schon im Terminal stand.
6
+ *
7
+ * 🔑 WARUM EINE EIGENE DATEI UND NICHT IN anmeldung.json: `kimal3 logout`
8
+ * loescht die Anmeldedatei. Laege der Merkzettel darin, kaeme nach jedem
9
+ * Abmelden jede Nachricht von vorn -- und ausgerechnet der Mensch, der sich
10
+ * gerade neu anmeldet, bekaeme die Wand.
11
+ *
12
+ * 🔴 BEIDE FUNKTIONEN WERFEN NIE. Ein kaputter oder schreibgeschuetzter
13
+ * Merkzettel darf keinen Befehl scheitern lassen (Spec US-8). Schlimmstenfalls
14
+ * sieht der Kunde denselben Hinweis zweimal -- das ist kein Schaden, ein
15
+ * abgebrochener `projekt neu` schon.
16
+ */
17
+ const ORDNER = join(homedir(), ".kimal3");
18
+ const DATEI = join(ORDNER, "gesehen.json");
19
+ export function leseGesehen() {
20
+ try {
21
+ const roh = JSON.parse(readFileSync(DATEI, "utf8"));
22
+ if (!roh || typeof roh !== "object" || Array.isArray(roh))
23
+ return {};
24
+ const raus = {};
25
+ for (const [k, v] of Object.entries(roh)) {
26
+ // Nur Zeichenketten uebernehmen. Wer die Datei von Hand bearbeitet hat,
27
+ // soll damit hoechstens seinen eigenen Merkzettel verlieren.
28
+ if (typeof v === "string")
29
+ raus[k] = v;
30
+ }
31
+ return raus;
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ export function merkeGesehen(id, tag) {
38
+ try {
39
+ const alles = leseGesehen();
40
+ /*
41
+ 🔑 ERST LOESCHEN, DANN SETZEN -- damit der Eintrag ans ENDE rutscht.
42
+ Ohne die erste Zeile behaelt ein schon vorhandener Schluessel seinen
43
+ alten Platz, und `slice(-20)` unten wirft dann ausgerechnet den weg, den
44
+ wir gerade geschrieben haben (Befund der vierten Review-Runde: der
45
+ Kommentar unten versprach genau das Gegenteil).
46
+
47
+ Die Folge waere klein -- derselbe Hinweis erschiene einmal zu viel --,
48
+ aber sie steht im Widerspruch zu einer Zusage im Code. Und eine Zusage
49
+ im Code ist das, was der naechste Leser glaubt, statt nachzurechnen.
50
+ */
51
+ delete alles[id];
52
+ alles[id] = tag;
53
+ /*
54
+ ⚠️ Nur die letzten 20 behalten. Ohne diese Zeile waechst die Datei mit
55
+ jeder je gezeigten Nachricht -- langsam, aber ohne Ende, und niemand
56
+ raeumt sie je auf.
57
+
58
+ 🔑 Warum das gefahrlos ist: `Object.entries` gibt Zeichenketten-Schluessel
59
+ in Einfuegereihenfolge zurueck, und der gerade gesetzte Eintrag ist immer
60
+ dabei -- entweder als letzter (neu) oder an seinem alten Platz
61
+ (aktualisiert). Was hinten abfaellt, ist die aelteste Nachricht; die
62
+ schlimmste Folge waere, dass sie noch einmal erscheint.
63
+ */
64
+ const paare = Object.entries(alles).slice(-20);
65
+ /*
66
+ ⚠️ `mode: 0o700` wie in anmeldung.ts -- und das ist KEINE Kosmetik
67
+ (Befund der ersten Review-Runde): `mkdirSync` mit `recursive: true`
68
+ aendert die Rechte eines Ordners, den es schon gibt, NICHT MEHR. Wer
69
+ diesen Ordner also zuerst anlegt, legt seine Rechte fuer immer fest.
70
+
71
+ Der Fall ist echt: Ein Mensch mit altem Skill, aber ohne Anmeldung,
72
+ tippt `kimal3` -- der Skill-Hinweis feuert, dieser Merkzettel entsteht,
73
+ und `~/.kimal3` bekaeme die Vorgabe-Rechte (auf einem gemeinsam
74
+ genutzten Rechner meist 0755). Ein spaeteres `kimal3 login` koennte sie
75
+ nicht mehr enger ziehen. Der Ausweis selbst bliebe zwar 0600 -- aber die
76
+ Haertung des Ordners waere still verloren, und "still" ist hier das
77
+ Schlimme.
78
+ */
79
+ mkdirSync(ORDNER, { recursive: true, mode: 0o700 });
80
+ writeFileSync(DATEI, JSON.stringify(Object.fromEntries(paare), null, 2), "utf8");
81
+ }
82
+ catch {
83
+ /* Merkzettel nicht schreibbar -- der Befehl laeuft trotzdem durch. */
84
+ }
85
+ }