kimal3 0.1.5 → 0.2.0
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/README.md +22 -1
- package/dist/api.js +138 -1
- package/dist/arbeitsplatz-dateien.js +268 -0
- package/dist/befehle/arbeitsplatz.js +209 -233
- package/dist/befehle/kontext-fragen.js +165 -0
- package/dist/befehle/kontext-gemeinsam.js +171 -0
- package/dist/befehle/kontext-hoch.js +203 -0
- package/dist/befehle/kontext-runter.js +254 -0
- package/dist/befehle/skill-install.js +110 -4
- package/dist/befehle/werkbank.js +389 -0
- package/dist/befehle-katalog.js +98 -3
- package/dist/fragen-lesen.js +178 -0
- package/dist/gesehen.js +85 -0
- package/dist/kimal3.js +178 -13
- package/dist/kontext-dateien.js +248 -0
- package/dist/kontext-stand.js +616 -0
- package/dist/nachricht.js +249 -0
- package/dist/version.js +37 -0
- package/dist/werkbank-name.js +217 -0
- package/package.json +1 -1
- package/skill/SKILL.md +130 -16
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Die reinen Rechnungen des Update-Kanals -- und sonst nichts.
|
|
3
|
+
*
|
|
4
|
+
* 🔑 WARUM DIESE DATEI NICHTS ANFASST: Kein fetch, kein readFileSync, kein
|
|
5
|
+
* `new Date()` ohne Uebergabe. Genau deshalb kann `npm run check:nachrichten`
|
|
6
|
+
* Faelle pruefen, die es in der Wirklichkeit nur durch Warten gaebe --
|
|
7
|
+
* "gestern gesehen", "0.10.0 gegen 0.9.0", "Stempel aus der Zukunft".
|
|
8
|
+
* Die Raender (Datei, Netz, Uhr) wohnen in gesehen.ts, api.ts und kimal3.ts.
|
|
9
|
+
*/
|
|
10
|
+
/** Der Stempel, den `kimal3 skill install` ans Ende der SKILL.md schreibt. */
|
|
11
|
+
export const STEMPEL_MARKE = "kimal3:erzeugt version=";
|
|
12
|
+
/**
|
|
13
|
+
* Zieht den Hinweis aus einer Server-Antwort -- oder gibt `null` zurueck.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ Jedes Feld einzeln pruefen, obwohl der Server unserer ist. Diese Zeilen
|
|
16
|
+
* laufen auf dem Rechner des Kunden und muessen auch dann halten, wenn dort
|
|
17
|
+
* eine ALTE CLI auf eine NEUE Antwort trifft oder umgekehrt. Genau dieser
|
|
18
|
+
* Versatz ist bei einem Werkzeug, das man selbst aktualisieren muss, der
|
|
19
|
+
* Normalfall -- kein Sonderfall.
|
|
20
|
+
*/
|
|
21
|
+
export function hinweisAusAntwort(koerper) {
|
|
22
|
+
if (!koerper || typeof koerper !== "object")
|
|
23
|
+
return null;
|
|
24
|
+
const roh = koerper.hinweis;
|
|
25
|
+
if (!roh || typeof roh !== "object")
|
|
26
|
+
return null;
|
|
27
|
+
const h = roh;
|
|
28
|
+
if (typeof h.id !== "string" || typeof h.text !== "string")
|
|
29
|
+
return null;
|
|
30
|
+
return {
|
|
31
|
+
id: h.id,
|
|
32
|
+
text: h.text,
|
|
33
|
+
gilt_unter_version: typeof h.gilt_unter_version === "string" ? h.gilt_unter_version : null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function teile(v) {
|
|
37
|
+
/*
|
|
38
|
+
⚠️ Ein Anhaengsel wie `-rc.1` oder `+build3` faellt vorher weg (Befund der
|
|
39
|
+
ersten Review-Runde). Ohne diese Zeile legt ein einziges `npm version
|
|
40
|
+
prerelease` den GANZEN Kanal still: `eigeneVersion()` gaebe "0.2.0-rc.1"
|
|
41
|
+
zurueck -- nicht `null`, also faehrt `hinweisText` weiter --, aber jeder
|
|
42
|
+
Vergleich antwortete `false`. Kein Netz-Hinweis, kein Skill-Hinweis, keine
|
|
43
|
+
Fehlermeldung. *Ein Merkmal, das sich lautlos abschaltet, faellt niemandem
|
|
44
|
+
auf.*
|
|
45
|
+
|
|
46
|
+
🔑 Bewusst NICHT nach Semver sortiert (dort gilt `0.2.0-rc.1` < `0.2.0`):
|
|
47
|
+
Diese Rechnung beantwortet "soll ich jemandem ein Update anbieten", und da
|
|
48
|
+
ist eine Vorabversion nah genug an ihrer Fassung. Die Genauigkeit waere
|
|
49
|
+
Aufwand fuer einen Fall, den es hier noch nie gab -- YAGNI.
|
|
50
|
+
*/
|
|
51
|
+
const stuecke = v.trim().split(/[-+]/)[0].split(".");
|
|
52
|
+
if (stuecke.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
const zahlen = [];
|
|
55
|
+
for (const s of stuecke) {
|
|
56
|
+
// ⚠️ Absichtlich streng: `Number("1a")` ist NaN, aber `parseInt("1a")` ist 1.
|
|
57
|
+
// Wer hier `parseInt` nimmt, macht aus dem Muellwert "bald" eine 0 und
|
|
58
|
+
// vergleicht dann munter weiter.
|
|
59
|
+
if (!/^\d+$/.test(s))
|
|
60
|
+
return null;
|
|
61
|
+
zahlen.push(Number(s));
|
|
62
|
+
}
|
|
63
|
+
return zahlen;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Ist `a` aelter als `b`? Zahl fuer Zahl, nicht als Text.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 DER FALL, DER DIESE FUNKTION RECHTFERTIGT: `"0.10.0" < "0.9.0"` ist als
|
|
69
|
+
* Textvergleich WAHR. Wer so vergleicht, haelt die neueste Fassung fuer die
|
|
70
|
+
* aeltere und drangsaliert genau die Kunden, die aktuell sind.
|
|
71
|
+
*
|
|
72
|
+
* Ist einer der beiden Werte kein Versionsstand, lautet die Antwort `false` --
|
|
73
|
+
* "im Zweifel schweigen" ist bei einem Nebenschauplatz die richtige Vorgabe.
|
|
74
|
+
*/
|
|
75
|
+
export function istAelter(a, b) {
|
|
76
|
+
const x = teile(a);
|
|
77
|
+
const y = teile(b);
|
|
78
|
+
if (!x || !y)
|
|
79
|
+
return false;
|
|
80
|
+
const laenge = Math.max(x.length, y.length);
|
|
81
|
+
for (let i = 0; i < laenge; i++) {
|
|
82
|
+
const links = x[i] ?? 0;
|
|
83
|
+
const rechts = y[i] ?? 0;
|
|
84
|
+
if (links !== rechts)
|
|
85
|
+
return links < rechts;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Welcher Tag ist in Deutschland gerade?
|
|
91
|
+
*
|
|
92
|
+
* ⚠️ NICHT `toISOString().slice(0,10)` -- das ist UTC. Um 00:30 deutscher Zeit
|
|
93
|
+
* waere "heute" der Vortag, und die Nachricht erschiene an einem Abend zweimal.
|
|
94
|
+
* `sv-SE` liefert von sich aus `JJJJ-MM-TT`; dieselbe Technik wie
|
|
95
|
+
* `heuteInDeutschland` in der App. Importieren laesst sie sich nicht --
|
|
96
|
+
* `cli/tsconfig.json` steht auf `rootDir: src` (Begruendung steht schon in
|
|
97
|
+
* projekt-zeilen.ts).
|
|
98
|
+
*/
|
|
99
|
+
export function heuteAus(d) {
|
|
100
|
+
return new Intl.DateTimeFormat("sv-SE", { timeZone: "Europe/Berlin" }).format(d);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Die ganze Entscheidung, als Funktion von vier Werten.
|
|
104
|
+
*/
|
|
105
|
+
export function sollZeigen(h, eigeneVersion, gesehen, heute) {
|
|
106
|
+
if (!h.id)
|
|
107
|
+
return false;
|
|
108
|
+
if (!h.text || h.text.trim() === "")
|
|
109
|
+
return false;
|
|
110
|
+
// Leeres Feld = alle sehen es. Sonst nur, wer aelter ist als die Grenze.
|
|
111
|
+
if (h.gilt_unter_version && !istAelter(eigeneVersion, h.gilt_unter_version))
|
|
112
|
+
return false;
|
|
113
|
+
// Hoechstens einmal am Tag -- auch bei zwanzig Befehlen.
|
|
114
|
+
if (gesehen[h.id] === heute)
|
|
115
|
+
return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Die Zeilen fuers Terminal.
|
|
120
|
+
*
|
|
121
|
+
* Bewusst Klartext mit `→`, wie der Rest der CLI: `cli/` hat null
|
|
122
|
+
* Abhaengigkeiten und nirgends ANSI-Farben. Ein gezeichneter Kasten braeche
|
|
123
|
+
* ausserdem in einem schmalen Terminal auseinander -- ein eingerueckter Satz
|
|
124
|
+
* nicht.
|
|
125
|
+
*/
|
|
126
|
+
export function formatiere(h) {
|
|
127
|
+
return `\n → ${saeubere(h.text)}\n`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Macht aus dem Nachrichtentext EINE harmlose Zeile.
|
|
131
|
+
*
|
|
132
|
+
* 🔴 WARUM UEBERHAUPT (Befund der vierten Review-Runde): Der Text kommt aus
|
|
133
|
+
* der Datenbank und wird in ein TERMINAL geschrieben. Steuerzeichen sind dort
|
|
134
|
+
* keine Zeichen, sondern Befehle -- eine ANSI-Fluchtfolge kann faerben, den
|
|
135
|
+
* Bildschirm loeschen oder den Wagen zurueckholen und damit Zeilen
|
|
136
|
+
* ueberschreiben, die schon dastanden. Der Kanal wuerde dann nicht mehr nur
|
|
137
|
+
* etwas HINZUFUEGEN, sondern die Ausgabe des Befehls veraendern.
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ Und Zeilenumbrueche muessen weg, weil die Ausgabe genau eine eingerueckte
|
|
140
|
+
* Zeile ist: Ein "\n" mitten im Text schoebe den Rest linksbuendig darunter --
|
|
141
|
+
* es saehe aus wie Ausgabe des Befehls, nicht wie ein Hinweis.
|
|
142
|
+
*
|
|
143
|
+
* 📌 Die Pruefung dazu stand vorher da und konnte nicht rot werden: Sie mass
|
|
144
|
+
* einen Vordruck ohne Steuerzeichen. *Eine gruene Pruefung ueber einen Wert,
|
|
145
|
+
* der nie anders aussieht, prueft den Vordruck, nicht den Code.*
|
|
146
|
+
*/
|
|
147
|
+
function saeubere(text) {
|
|
148
|
+
return text
|
|
149
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
150
|
+
.replace(/\s+/g, " ")
|
|
151
|
+
.trim();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Zieht `0.1.6` aus `<!-- kimal3:erzeugt version=0.1.6 -->`.
|
|
155
|
+
*
|
|
156
|
+
* 🔴 DER LETZTE TREFFER GILT, nicht der erste (Befund der fuenften
|
|
157
|
+
* Review-Runde). Der echte Stempel steht immer am DATEIENDE -- aber die
|
|
158
|
+
* SKILL.md ist ein Text ueber dieses Werkzeug und beschreibt inzwischen selbst,
|
|
159
|
+
* wie der Kanal funktioniert. Nennt sie eines Tages ein Beispiel wie
|
|
160
|
+
* `version=9.9.9`, gewaenne der Beispieltext gegen den echten Stempel:
|
|
161
|
+
* `skillZustand` antwortete fuer immer "still", und Weg 2 waere lautlos tot.
|
|
162
|
+
*
|
|
163
|
+
* *Ein Dokument, das seine eigene Maschinenlesbarkeit beschreibt, wird
|
|
164
|
+
* irgendwann versehentlich zu seinen eigenen Daten.*
|
|
165
|
+
*/
|
|
166
|
+
export function stempelAus(inhalt) {
|
|
167
|
+
const treffer = [...inhalt.matchAll(/kimal3:erzeugt version=([0-9][0-9.]*)/g)];
|
|
168
|
+
return treffer.length > 0 ? treffer[treffer.length - 1][1] : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Die DREI Zustaende aus US-4/US-5 -- und sie sind ausdruecklich drei, nicht
|
|
172
|
+
* zwei:
|
|
173
|
+
*
|
|
174
|
+
* | Was liegt dort | Antwort |
|
|
175
|
+
* |-----------------------|------------|
|
|
176
|
+
* | Datei fehlt | still | <- nie installiert
|
|
177
|
+
* | Datei ohne Stempel | veraltet | <- 0.1.4 und aelter
|
|
178
|
+
* | Stempel aelter | veraltet |
|
|
179
|
+
* | Stempel gleich/neuer | still | <- niemand wird rueckwaerts gedraengt
|
|
180
|
+
*
|
|
181
|
+
* ⚠️ Die ersten beiden sehen im Dateisystem fast gleich aus und bedeuten das
|
|
182
|
+
* Gegenteil. `null` heisst hier "es gibt keine Datei", NICHT "leer".
|
|
183
|
+
*/
|
|
184
|
+
export function skillZustand(inhalt, eigeneVersion) {
|
|
185
|
+
if (inhalt === null)
|
|
186
|
+
return "still";
|
|
187
|
+
const stempel = stempelAus(inhalt);
|
|
188
|
+
if (stempel === null)
|
|
189
|
+
return "veraltet";
|
|
190
|
+
return istAelter(stempel, eigeneVersion) ? "veraltet" : "still";
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* ⚠️ Sie nennt den Befehl, sie FUEHRT IHN NICHT AUS. Ein Befehl darf nicht
|
|
194
|
+
* mehr tun, als sein Name verspricht -- die Regel steht schon in kimal3.ts.
|
|
195
|
+
*/
|
|
196
|
+
export function skillMeldung() {
|
|
197
|
+
return "\n → Deine kimal3-Skilldateien sind aelter als die CLI. kimal3 skill install\n";
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Zerlegt den Kopf `X-Kimal3-Kontext-Stand`: `<firmen-id>:<stand>` (Task 10).
|
|
201
|
+
*
|
|
202
|
+
* ⚠️ AM ERSTEN DOPPELPUNKT TRENNEN, nicht am `split(":")[1]`: `stand` ist oft
|
|
203
|
+
* ein ISO-Zeitstempel und traegt selbst Doppelpunkte
|
|
204
|
+
* (`2026-08-21T07:12:33.456Z`). Ein `split` wuerde ihn mitten durchschneiden.
|
|
205
|
+
*
|
|
206
|
+
* Fehlt der Kopf, ist er leer, hat keinen Doppelpunkt oder ist eine Seite
|
|
207
|
+
* leer -> `null`. Diese Datei rechnet nur; SIE PRUEFT NICHT, ob die Firmen-ID
|
|
208
|
+
* echt ist oder der Anrufer ihr Mitglied ist -- das ist Datenbankzugriff und
|
|
209
|
+
* gehoert der Aufrufstelle (`src/lib/cli/nachricht.ts`).
|
|
210
|
+
*/
|
|
211
|
+
export function kontextStandAusKopf(wert) {
|
|
212
|
+
if (!wert)
|
|
213
|
+
return null;
|
|
214
|
+
const trenner = wert.indexOf(":");
|
|
215
|
+
if (trenner <= 0)
|
|
216
|
+
return null;
|
|
217
|
+
const firmaId = wert.slice(0, trenner);
|
|
218
|
+
const stand = wert.slice(trenner + 1);
|
|
219
|
+
if (stand === "")
|
|
220
|
+
return null;
|
|
221
|
+
return { firmaId, stand };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Die ZWEITE Quelle des Update-Kanals (Task 10) -- rein: Sie bekommt den
|
|
225
|
+
* lokalen Stand (aus dem Kopf, schon geparst) und den server-gefuehrten
|
|
226
|
+
* Stand samt Firmennamen fertig nachgeschlagen und ENTSCHEIDET nur noch.
|
|
227
|
+
*
|
|
228
|
+
* 🔴 WARUM DER SERVER-STAND UND NICHT DER ROHE KOPF VERGLICHEN WIRD: Der
|
|
229
|
+
* Server weiss nicht, was auf der Platte des Kunden liegt -- die CLI schickt
|
|
230
|
+
* ihren Stand, die App vergleicht ihn nur mit ihrem eigenen (Bauplan Step 2).
|
|
231
|
+
*
|
|
232
|
+
* 🔑 `id: kontext-<serverStand>` (Step 3): Die Tagessperre der CLI merkt sich
|
|
233
|
+
* Hinweise nach `id` -- aendert die Firma den Kontext ein zweites Mal am
|
|
234
|
+
* selben Tag, aendert sich `serverStand` und damit automatisch die `id`. Der
|
|
235
|
+
* Hinweis erscheint also je NEUEM Stand genau einmal, ohne dass diese
|
|
236
|
+
* Funktion selbst je ein Datum ansieht.
|
|
237
|
+
*
|
|
238
|
+
* 🔑 US-8: `firmenName` steht IM TEXT, nicht nur die ID -- wer in zwei Firmen
|
|
239
|
+
* ist, muss lesen koennen, WELCHE gemeint ist.
|
|
240
|
+
*/
|
|
241
|
+
export function firmenHinweisAus(lokalerStand, serverStand, firmenName) {
|
|
242
|
+
if (lokalerStand === serverStand)
|
|
243
|
+
return null;
|
|
244
|
+
return {
|
|
245
|
+
id: `kontext-${serverStand}`,
|
|
246
|
+
text: `${firmenName} hat den Firmenkontext aktualisiert. kimal3 kontext runter`,
|
|
247
|
+
gilt_unter_version: null,
|
|
248
|
+
};
|
|
249
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Die eigene Versionsnummer -- GELESEN, nie abgetippt.
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ Die Regel dahinter ist gemessen (QA-Befund B-8, 2026-08-18): Stuende die
|
|
6
|
+
* Zahl als Text im Code, sagte die CLI nach dem naechsten `npm version` die
|
|
7
|
+
* alte Nummer. Dieselbe Zahl an zwei Tueren mit zwei Werten -- ein Muster, das
|
|
8
|
+
* in diesem Projekt schon mehrfach zugeschlagen hat.
|
|
9
|
+
*
|
|
10
|
+
* 📌 `../package.json` stimmt aus `dist/` UND aus `src/`: Beide liegen eine
|
|
11
|
+
* Ebene unter `cli/`. Deshalb steht hier kein fester Pfad.
|
|
12
|
+
*
|
|
13
|
+
* 🔴 RUECKGABE `null`, NICHT "0.0.0" und auch nicht "unbekannt".
|
|
14
|
+
* Diese Funktion hat zwei Aufrufer mit gegensaetzlichem Bedarf:
|
|
15
|
+
* - `versionsZeilen()` ZEIGT den Wert einem Menschen. Dort ist "unbekannt"
|
|
16
|
+
* die ehrliche Antwort -- eine erfundene "0.0.0" waere eine Luege auf dem
|
|
17
|
+
* Bildschirm.
|
|
18
|
+
* - `hinweisAusgeben()` RECHNET damit. Dort ist "unbekannt" Muell, der jeden
|
|
19
|
+
* Versionsvergleich schweigen laesst.
|
|
20
|
+
* `null` zwingt beide, ihren eigenen Fall zu entscheiden, statt einen
|
|
21
|
+
* Ersatzwert zu erben, der nur fuer den anderen passt.
|
|
22
|
+
*/
|
|
23
|
+
export function eigeneVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const roh = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
26
|
+
if (roh && typeof roh === "object" && "version" in roh) {
|
|
27
|
+
const v = roh.version;
|
|
28
|
+
if (typeof v === "string")
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Kein Absturz, wenn die Datei fehlt oder Unsinn enthaelt. Der Aufrufer
|
|
34
|
+
// entscheidet, was "keine Nummer" bei ihm bedeutet.
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wie eine Werkbank heisst — **eine** Regel fuer beide Auslieferungswege.
|
|
3
|
+
*
|
|
4
|
+
* 🔑 **Warum das eine eigene Datei ist** (2026-08-20, bei der Umbenennung von
|
|
5
|
+
* `meine-werkbank` auf `werkbank-<name>`): Der Ordnername entsteht an ZWEI
|
|
6
|
+
* Stellen. Der Server setzt ihn beim Ausliefern (Zip-Download auf `/pro` UND
|
|
7
|
+
* `/api/cli/arbeitsplatz`), und die CLI setzt ihn noch einmal, wenn der Mensch
|
|
8
|
+
* einen Namen mitgegeben hat. Waeren das zwei Regeln, hiesse derselbe Mensch je
|
|
9
|
+
* nach Weg anders — genau die Sorte Unterschied, die man erst Tage spaeter
|
|
10
|
+
* bemerkt.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ **DIESE DATEI IMPORTIERT NICHTS**, aus demselben Grund wie
|
|
13
|
+
* `befehle-katalog.ts`: Die CLI baut mit NodeNext (Import-Pfade enden auf
|
|
14
|
+
* `.js`), die App mit dem Bundler. Eine Datei ohne eigene Importe liest sich aus
|
|
15
|
+
* beiden Welten ohne Uebersetzung.
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **Wozu das Ganze** (Entscheidung Dennis, 2026-08-20): *Besitz steht im
|
|
18
|
+
* Ordnernamen, nicht in einem Possessivpronomen.* Der Vorgaenger `meine-werkbank`
|
|
19
|
+
* hiess bei JEDEM gleich — liegen zwei davon nebeneinander oder gehen sie zu
|
|
20
|
+
* GitHub, ist nicht mehr erkennbar, wem welche gehoert. Wo ein Name steht
|
|
21
|
+
* (`werkbank-sabine`), gehoert der Ordner einer Person; wo keiner steht
|
|
22
|
+
* (`kontext/`, `KI-Arbeitsplatz/`), gehoert er allen. *Ein Nutzer muss diese
|
|
23
|
+
* Regel nicht lernen — er sieht sie im Baum.*
|
|
24
|
+
*/
|
|
25
|
+
/** Das Praefix, an dem man eine Werkbank erkennt. Bewacht von `check:arbeitsplatz` (11b). */
|
|
26
|
+
export const WERKBANK_PRAEFIX = "werkbank-";
|
|
27
|
+
/**
|
|
28
|
+
* Wie lang der Namensteil hoechstens werden darf.
|
|
29
|
+
*
|
|
30
|
+
* Nicht aus Geschmack: Windows deckelt einen ganzen Pfad bei 260 Zeichen, und
|
|
31
|
+
* dieser Ordner traegt darunter noch `projekte/<projekt>/referenzen/…`. Ein
|
|
32
|
+
* ellenlanger Name aus einer Sammel-Adresse wuerde erst dort unten Fehler
|
|
33
|
+
* erzeugen — an einer Stelle, an der niemand mehr an den Ordnernamen denkt.
|
|
34
|
+
*/
|
|
35
|
+
const MAX_LAENGE = 40;
|
|
36
|
+
/**
|
|
37
|
+
* Aus dem, was ein Mensch eingibt (oder aus seiner Adresse kommt), ein Ordnername.
|
|
38
|
+
*
|
|
39
|
+
* "Sabine" → "werkbank-sabine"
|
|
40
|
+
* "Sabine Müller" → "werkbank-sabine-mueller"
|
|
41
|
+
* "werkbank-sabine" → "werkbank-sabine" (nicht doppelt)
|
|
42
|
+
* "sabine@arno.de" → "werkbank-sabine" (der Teil vor dem @)
|
|
43
|
+
* "!!!" → null (nichts Brauchbares uebrig)
|
|
44
|
+
*
|
|
45
|
+
* 🔑 **Warum umgeschrieben und nicht abgelehnt wird:** Der Name landet als
|
|
46
|
+
* Ordner auf einer echten Platte. Ein `/` darin waere ein Unterordner, ein `:`
|
|
47
|
+
* unter Windows ein Laufwerk, ein Leerzeichen ein zweites Argument im naechsten
|
|
48
|
+
* `cd`. Wer „Sabine Müller" eingibt, hat nichts falsch gemacht — er hat einen
|
|
49
|
+
* Namen genannt, und Namen duerfen Umlaute und Leerzeichen haben.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ **Umlaute werden AUSGESCHRIEBEN, nicht entfernt.** `ü` einfach zu streichen
|
|
52
|
+
* machte aus „Müller" ein „Mller"; die deutsche Ersatzschreibung `ue` ist die,
|
|
53
|
+
* die jeder Mensch hier erwartet. Der Weg ueber `normalize("NFD")` allein reicht
|
|
54
|
+
* dafuer NICHT — er zerlegt `ü` in `u` + Trema und liefert damit „Muller".
|
|
55
|
+
* Deshalb stehen die vier deutschen Zeichen ausdruecklich davor.
|
|
56
|
+
*
|
|
57
|
+
* 📌 Gibt `null` zurueck, wenn nichts uebrig bleibt. Das ist **kein** Fehlerfall
|
|
58
|
+
* dieser Funktion, sondern eine Auskunft: Der Aufrufer entscheidet, ob er
|
|
59
|
+
* absagt (die CLI, wo ein Mensch etwas getippt hat) oder still beim Vordruck
|
|
60
|
+
* bleibt (der Server, wo niemand gefragt wurde).
|
|
61
|
+
*/
|
|
62
|
+
export function werkbankOrdnerAus(roh) {
|
|
63
|
+
/*
|
|
64
|
+
🔴 **Spitze Klammern sind eine ABSAGE, keine Zeichen zum Wegwerfen**
|
|
65
|
+
(Review-Runde 5 zu PR #25). Gemessen: `"<dein-vorname>"` wurde zu
|
|
66
|
+
`werkbank-dein-vorname` -- ein Ordner, der plausibel aussieht und den
|
|
67
|
+
niemand wollte.
|
|
68
|
+
|
|
69
|
+
Und die Zeichenkette kommt nicht aus dem Nichts: Beide Absagen dieser CLI
|
|
70
|
+
und die Tabelle in `SKILL.md` legen sie dem Menschen woertlich hin
|
|
71
|
+
(`kimal3 werkbank neu <dein-vorname>`). In einer rohen Shell rettet der
|
|
72
|
+
Zufall -- dort ist `<` eine Umleitung und der Aufruf scheitert vorher. In
|
|
73
|
+
Claude Code, wo der Aufruf als Zeichenkette weitergereicht wird, rettet
|
|
74
|
+
nichts.
|
|
75
|
+
|
|
76
|
+
*Genau der Fehler, gegen den `Anleitung.tsx` den Kopier-Kasten entworfen
|
|
77
|
+
hat: „Ein Platzhalter darin wird mitkopiert und ergibt einen Ordner, den
|
|
78
|
+
niemand wollte."* Er stand hier eine Ebene tiefer noch offen.
|
|
79
|
+
*/
|
|
80
|
+
if (roh.includes("<") || roh.includes(">"))
|
|
81
|
+
return null;
|
|
82
|
+
const vorDemAt = roh.split("@")[0] ?? "";
|
|
83
|
+
const grundform = vorDemAt
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replaceAll("ä", "ae")
|
|
86
|
+
.replaceAll("ö", "oe")
|
|
87
|
+
.replaceAll("ü", "ue")
|
|
88
|
+
.replaceAll("ß", "ss")
|
|
89
|
+
// Alles andere mit Akzent auf den Grundbuchstaben zurueck (é → e, ç → c).
|
|
90
|
+
.normalize("NFD")
|
|
91
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
92
|
+
// Was jetzt noch kein a–z oder 0–9 ist, wird zum Trennstrich -- und
|
|
93
|
+
// mehrere Trennstriche werden einer.
|
|
94
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
95
|
+
.replace(/^-+|-+$/g, "");
|
|
96
|
+
// Ein bereits vollstaendiger Ordnername soll nicht `werkbank-werkbank-sabine`
|
|
97
|
+
// werden. Das ist kein Sonderfall fuer Feinschmecker: Die Hilfe von
|
|
98
|
+
// `werkbank neu` hat bis heute genau diese Schreibweise beigebracht
|
|
99
|
+
// (`kimal3 werkbank neu werkbank-sabine`), und alte Anleitungen ueberleben
|
|
100
|
+
// laenger als der Code, den sie beschreiben.
|
|
101
|
+
const ohnePraefix = grundform.startsWith(WERKBANK_PRAEFIX)
|
|
102
|
+
? grundform.slice(WERKBANK_PRAEFIX.length)
|
|
103
|
+
: grundform;
|
|
104
|
+
// ⚠️ Erst kuerzen, DANN den Trennstrich am Ende wegnehmen: Faellt der Schnitt
|
|
105
|
+
// mitten in „sabine-mueller", bliebe sonst ein „werkbank-sabine-" stehen.
|
|
106
|
+
const gekuerzt = ohnePraefix.slice(0, MAX_LAENGE).replace(/-+$/g, "");
|
|
107
|
+
if (gekuerzt === "")
|
|
108
|
+
return null;
|
|
109
|
+
return `${WERKBANK_PRAEFIX}${gekuerzt}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Die Vorlage auf einen anderen Werkbank-Namen umschreiben — **Pfade und Texte**.
|
|
113
|
+
*
|
|
114
|
+
* 🔑 **Warum auch die Texte** — und warum die Begruendung dafuer sich am
|
|
115
|
+
* 2026-08-20 zweimal gedreht hat:
|
|
116
|
+
*
|
|
117
|
+
* Zuerst hiess sie: *„Die `README.md` sagt woertlich `cd werkbank-dein-name`,
|
|
118
|
+
* und `check:arbeitsplatz` (Pruefung 11) haelt das fest."* Das stimmte —
|
|
119
|
+
* bis Review-Runde 3 zeigte, dass ein Name im GEMEINSAMEN Firmentext den Namen
|
|
120
|
+
* EINES Menschen ist, und Pruefung 11 daraufhin umgedreht wurde: Sie verbietet
|
|
121
|
+
* ihn dort jetzt.
|
|
122
|
+
*
|
|
123
|
+
* 🔬 **Heute nennt deshalb keine ausgelieferte Datei den Werkbank-Namen im
|
|
124
|
+
* Text** (nachgemessen: `grep` findet ihn nur im Ordnernamen). Die Ersetzung
|
|
125
|
+
* hat also aktuell **nichts zu tun** — und bleibt trotzdem drin, als Netz fuer
|
|
126
|
+
* den Fall, dass jemand in eine Werkbank-Datei `werkbank-sabine/projekte/`
|
|
127
|
+
* schreibt. Dort verbietet es keine Wache, und der Satz zeigte beim naechsten
|
|
128
|
+
* Menschen auf einen fremden Ordner.
|
|
129
|
+
*
|
|
130
|
+
* *Ein Netz, das heute niemand braucht, ist ehrlich zu beschreiben — nicht mit
|
|
131
|
+
* einer Zusicherung zu begruenden, die inzwischen das Gegenteil sagt.*
|
|
132
|
+
*
|
|
133
|
+
* ⚠️ Ersetzt wird der ganze Ordnername, nicht das Praefix — sonst traefe es
|
|
134
|
+
* jeden Text, der irgendeine Werkbank erwaehnt.
|
|
135
|
+
*
|
|
136
|
+
* 🔴 **Und das reicht NICHT, um erklaerende Beispiele zu schuetzen**
|
|
137
|
+
* (Review-Runde 1 zu PR #25). Hier stand, die Beispiele `werkbank-sabine` in der
|
|
138
|
+
* Vorlage blieben stehen. Das stimmt nur, solange `alt` der Vordruck ist — bei
|
|
139
|
+
* der CLI ist `alt` aber der Name, den der SERVER gesetzt hat. Heisst ein Konto
|
|
140
|
+
* `werkbank-sabine` und ruft `arbeitsplatz neu jonas` auf, wird aus dem
|
|
141
|
+
* erklaerenden Beispiel ebenfalls `werkbank-jonas`, und der Satz liest sich
|
|
142
|
+
* *„traegt seinen Namen: `werkbank-jonas`, `werkbank-jonas`"*.
|
|
143
|
+
*
|
|
144
|
+
* 🔑 Geschuetzt sind Beispiele deshalb nicht hier, sondern in der VORLAGE: Sie
|
|
145
|
+
* heissen `werkbank-<vorname>`, und diese Form kann kein echter Ordner tragen —
|
|
146
|
+
* `<` und `>` ueberleben `werkbankOrdnerAus` nicht. `check:arbeitsplatz` (11d)
|
|
147
|
+
* haelt das fest. *Eine Zusicherung, die nur fuer einen der beiden Aufrufer
|
|
148
|
+
* gilt, ist keine Zusicherung dieser Funktion.*
|
|
149
|
+
*
|
|
150
|
+
* 📌 Ist `alt` gleich `neu`, kommt die Liste unveraendert zurueck. Der Aufrufer
|
|
151
|
+
* muss also nicht selbst pruefen, ob sich etwas aendert.
|
|
152
|
+
*/
|
|
153
|
+
export function werkbankUmbenennen(dateien, alt, neu) {
|
|
154
|
+
if (alt === "" || neu === "" || alt === neu)
|
|
155
|
+
return [...dateien];
|
|
156
|
+
const altesPraefix = `${alt}/`;
|
|
157
|
+
const neuesPraefix = `${neu}/`;
|
|
158
|
+
/*
|
|
159
|
+
🔴 **Mit Wortgrenze, nicht mit `replaceAll`** (Review-Runde 5 zu PR #25).
|
|
160
|
+
Gemessen: Ein nacktes `replaceAll("werkbank-sabine", "werkbank-jonas")`
|
|
161
|
+
macht aus `werkbank-sabine-alt` ein `werkbank-jonas-alt` -- es zerlegt jedes
|
|
162
|
+
laengere Token, das mit dem Namen anfaengt.
|
|
163
|
+
|
|
164
|
+
Heute stolpert niemand darueber, weil `check:arbeitsplatz` (11d) solche
|
|
165
|
+
Token aus der Vorlage haelt. Aber der Doku-Block oben verkauft die
|
|
166
|
+
Inhalts-Ersetzung ausdruecklich als **Netz fuer kuenftige Texte** -- und ein
|
|
167
|
+
Netz mit einem Loch ist genau das, was man erst am Tag des Sturzes merkt.
|
|
168
|
+
|
|
169
|
+
Die Grenze gilt auf BEIDEN Seiten: `xwerkbank-sabine` ist ebenso wenig
|
|
170
|
+
gemeint wie `werkbank-sabine-alt`. Erlaubt sind nur Zeichen, die in keinem
|
|
171
|
+
Ordnernamen vorkommen koennen -- alles ausser `[a-z0-9-]`.
|
|
172
|
+
*/
|
|
173
|
+
const grenze = new RegExp(`(?<![a-z0-9-])${maskiere(alt)}(?![a-z0-9-])`, "g");
|
|
174
|
+
return dateien.map((datei) => ({
|
|
175
|
+
...datei,
|
|
176
|
+
pfad: datei.pfad.startsWith(altesPraefix)
|
|
177
|
+
? `${neuesPraefix}${datei.pfad.slice(altesPraefix.length)}`
|
|
178
|
+
: datei.pfad,
|
|
179
|
+
inhalt: datei.inhalt.replace(grenze, neu),
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Welcher Unterordner die Werkbank ist — **abgeleitet, nicht getippt**.
|
|
184
|
+
*
|
|
185
|
+
* Erkennungsregel: der einzige Unterordner, der DIREKT eine eigene `CLAUDE.md`
|
|
186
|
+
* traegt (Pfad `<ordner>/CLAUDE.md`, genau zwei Teile). Dass es genau einen gibt,
|
|
187
|
+
* sichert `check:arbeitsplatz` (Pruefung 10) — ohne diese Wache waehlte die Regel
|
|
188
|
+
* bei mehreren stumm den ersten.
|
|
189
|
+
*
|
|
190
|
+
* 🔴 Diese Regel stand vorher ZWEIMAL im Projekt, wortgleich: einmal in
|
|
191
|
+
* `cli/src/arbeitsplatz-dateien.ts` (Befund R2-3) und einmal in
|
|
192
|
+
* `src/lib/arbeitsplatz/ausliefern.ts` (Runde 3). Beide Male war die Begruendung
|
|
193
|
+
* „nicht austippen" — und trotzdem wurde die Ableitung selbst kopiert.
|
|
194
|
+
* *Zwei Kopien einer Ableitung sind dieselbe zweite Wahrheit wie zwei Kopien des
|
|
195
|
+
* Namens, nur besser getarnt.*
|
|
196
|
+
*/
|
|
197
|
+
export function werkbankAusDateien(dateien) {
|
|
198
|
+
for (const datei of dateien) {
|
|
199
|
+
const teile = datei.pfad.split("/");
|
|
200
|
+
if (teile.length === 2 && teile[1] === "CLAUDE.md")
|
|
201
|
+
return teile[0];
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Sonderzeichen eines regulaeren Ausdrucks entschaerfen.
|
|
207
|
+
*
|
|
208
|
+
* 🔑 Der Name kommt aus einer Server-Antwort. `werkbankOrdnerAus` laesst zwar
|
|
209
|
+
* nur `[a-z0-9-]` durch — aber `werkbankUmbenennen` bekommt `alt` NICHT von
|
|
210
|
+
* dort, sondern aus der gelieferten Dateiliste (`werkbankAusDateien`). Was von
|
|
211
|
+
* aussen kommt, wird nie zum Muster. *Eine Regel, die nur fuer den einen
|
|
212
|
+
* Aufrufer gilt, ist keine Regel dieser Funktion* -- dieselbe Lehre wie bei der
|
|
213
|
+
* Zusicherung ueber die Beispiele.
|
|
214
|
+
*/
|
|
215
|
+
function maskiere(text) {
|
|
216
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
217
|
+
}
|