kimal3 0.1.6 β 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/api.js +112 -0
- package/dist/arbeitsplatz-dateien.js +84 -22
- package/dist/argumente.js +150 -0
- package/dist/befehle/arbeitsplatz.js +139 -17
- package/dist/befehle/kontext-fragen.js +182 -0
- package/dist/befehle/kontext-gemeinsam.js +185 -0
- package/dist/befehle/kontext-hoch.js +236 -0
- package/dist/befehle/kontext-runter.js +305 -0
- package/dist/befehle/skill-install.js +51 -17
- package/dist/befehle/werkbank.js +128 -17
- package/dist/befehle-katalog.js +58 -2
- package/dist/firmen-liste.js +4 -2
- package/dist/fragen-lesen.js +178 -0
- package/dist/kimal3.js +29 -2
- package/dist/kontext-dateien.js +248 -0
- package/dist/kontext-stand.js +870 -0
- package/dist/nachricht.js +137 -2
- package/package.json +1 -1
- package/skill/SKILL.md +106 -11
- package/skill/commands/k3.md +59 -18
- package/skill/commands/k3kontext.md +89 -0
- /package/skill/commands/{k3firma.md β k3analyse.md} +0 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { zerlege } from "../argumente.js";
|
|
2
|
+
import { ruf } from "../api.js";
|
|
3
|
+
import { heuteAus } from "../nachricht.js";
|
|
4
|
+
import { istUnsichererPfad } from "../arbeitsplatz-dateien.js";
|
|
5
|
+
import { LEER, VERWALTETE_DATEIEN, alsMerkzettel, sicherungsOrdner, sicherungsZiel, lageAusMerkzetteln, ratNachUnberuehrt, } from "../kontext-stand.js";
|
|
6
|
+
import { leseEineDatei, leseMerkzettelDatei, schreibeMerkzettelDatei, schreibeVerwalteteDatei, sichereDatei, } from "../kontext-dateien.js";
|
|
7
|
+
import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js";
|
|
8
|
+
/**
|
|
9
|
+
* Holt den Firmenkontext von der App und legt ihn in den verwalteten Dateien
|
|
10
|
+
* des gefundenen Arbeitsplatzes ab -- `CLAUDE.md` und die fuenf Dateien in
|
|
11
|
+
* `kontext/`.
|
|
12
|
+
*
|
|
13
|
+
* π΄ **`kontext/kurzprofil.md` ist seit Ruling K-24 (Task 7) dabei.** Sie wurde
|
|
14
|
+
* beim ERSTEN Ausliefern gefuellt und danach nie wieder aufgefrischt -- waehrend
|
|
15
|
+
* sie selbst dem Kunden zusicherte, sie werde βzentral in der App gepflegt".
|
|
16
|
+
*
|
|
17
|
+
* π΄ **R2 (Spec Β§14): Dieser Befehl fragt NIE `git status`.** Er entscheidet
|
|
18
|
+
* ausschliesslich ueber den Merkzettel (`.kimal3-stand.json`), egal ob der
|
|
19
|
+
* Ordner ein Git-Repo ist oder nicht -- sonst gaebe es zwei Verhalten fuer
|
|
20
|
+
* denselben Befehl, und das schwerer erklaerbare traefe ausgerechnet den, der
|
|
21
|
+
* Git benutzt.
|
|
22
|
+
*
|
|
23
|
+
* β **Unterhalb von `werkbank-<name>/` wird nichts geschrieben** (Spec Β§4).
|
|
24
|
+
* Dieser Befehl kennt genau die Pfade aus `VERWALTETE_DATEIEN`, und in keinem
|
|
25
|
+
* davon steht ein Personenname.
|
|
26
|
+
*
|
|
27
|
+
* π **Ruling T9-2:** Die Gegenrichtung, `kontextHoch`, wohnt seit dem Split
|
|
28
|
+
* von `kontext.ts` in `kontext-hoch.ts`. Beide teilen sich Fundregel,
|
|
29
|
+
* Firmenwahl-Absage und die `--firma`/`--firma-id`-Optionen ueber
|
|
30
|
+
* `kontext-gemeinsam.ts`.
|
|
31
|
+
*/
|
|
32
|
+
const HILFE = `
|
|
33
|
+
kimal3 kontext runter [--firma "Name" | --firma-id "β¦"]
|
|
34
|
+
|
|
35
|
+
Holt den Firmenkontext (CLAUDE.md + die fuenf Dateien in kontext/) und legt
|
|
36
|
+
ihn in deinem KI-Arbeitsplatz ab. Was du selbst geaendert hast, fasst der
|
|
37
|
+
Befehl nicht an -- er sagt dir, wie du es einbringst.
|
|
38
|
+
|
|
39
|
+
--firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
|
|
40
|
+
Die Absage nennt dir die Namen als fertige Befehle.
|
|
41
|
+
|
|
42
|
+
--firma-id "β¦" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
|
|
43
|
+
Auch diese Absage nennt dir fertige Befehle.
|
|
44
|
+
`;
|
|
45
|
+
export async function kontextRunter(argv) {
|
|
46
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
47
|
+
console.log(HILFE);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
const zerlegt = zerlege(argv, ERLAUBT);
|
|
51
|
+
if (!zerlegt.ok) {
|
|
52
|
+
console.error(zerlegt.meldung);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
// π Seit Task 7 gemeinsam mit `kontext hoch` und `kontext fragen`
|
|
56
|
+
// (`arbeitsplatzOderAbsage`). Hier standen dieselben vier Zeilen abgetippt;
|
|
57
|
+
// bei drei Aufrufern ist das keine Wiederholung mehr, sondern eine Falle --
|
|
58
|
+
// wer eine Zeile schaerft, schaerft eine von dreien.
|
|
59
|
+
const ordner = arbeitsplatzOderAbsage();
|
|
60
|
+
if (ordner === null)
|
|
61
|
+
return 1;
|
|
62
|
+
const wunsch = zerlegt.werte.optionen.firma;
|
|
63
|
+
const wunschId = zerlegt.werte.optionen["firma-id"];
|
|
64
|
+
const suche = new URLSearchParams();
|
|
65
|
+
if (wunsch !== undefined)
|
|
66
|
+
suche.set("firma", wunsch);
|
|
67
|
+
if (wunschId !== undefined)
|
|
68
|
+
suche.set("firma-id", wunschId);
|
|
69
|
+
const pfad = suche.toString() ? `/api/cli/kontext?${suche.toString()}` : "/api/cli/kontext";
|
|
70
|
+
const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true });
|
|
71
|
+
if (antwort.status !== 200) {
|
|
72
|
+
return absage(antwort, "kontext runter");
|
|
73
|
+
}
|
|
74
|
+
// π΄ Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dasselbe Muster wie
|
|
75
|
+
// in `firma-daten.ts`. `ruf` verschluckt Parse-Fehler zu `null`.
|
|
76
|
+
if (antwort.koerper === null || typeof antwort.koerper !== "object") {
|
|
77
|
+
console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
|
|
78
|
+
return 3;
|
|
79
|
+
}
|
|
80
|
+
const daten = alsKontextAntwort(antwort.koerper);
|
|
81
|
+
if (daten === null) {
|
|
82
|
+
console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
|
|
83
|
+
return 3;
|
|
84
|
+
}
|
|
85
|
+
const lokalerMerkzettel = leseMerkzettelDatei(ordner);
|
|
86
|
+
const heute = heuteAus(new Date());
|
|
87
|
+
const geschrieben = [];
|
|
88
|
+
// Punkt A-1: die Teilmenge von `geschrieben`, bei der NICHT die Firma etwas
|
|
89
|
+
// geaendert hat, sondern wir unsere eigene Vorlage. Der Mensch soll den
|
|
90
|
+
// Unterschied lesen koennen -- sonst wundert er sich, warum sich eine Datei
|
|
91
|
+
// aendert, an der seine Firma nachweislich nichts getan hat.
|
|
92
|
+
const vorlageErneuert = [];
|
|
93
|
+
const gesicherte = [];
|
|
94
|
+
const unberuehrt = [];
|
|
95
|
+
for (const verwalteterPfad of VERWALTETE_DATEIEN) {
|
|
96
|
+
const serverDatei = daten.dateien.find((d) => d.pfad === verwalteterPfad);
|
|
97
|
+
if (!serverDatei) {
|
|
98
|
+
console.error(`Die Antwort enthaelt keine Datei β${verwalteterPfad}". Bitte melden.`);
|
|
99
|
+
return 3;
|
|
100
|
+
}
|
|
101
|
+
// Zweites Netz -- s. Kommentar an `schreibeVerwalteteDatei`: `pfad` kommt
|
|
102
|
+
// hier aus der eigenen Iteration, nicht roh vom Server, trotzdem geprueft.
|
|
103
|
+
if (istUnsichererPfad(serverDatei.pfad)) {
|
|
104
|
+
console.error(`Die Antwort enthaelt einen unsicheren Pfad: β${serverDatei.pfad}". Bitte melden.`);
|
|
105
|
+
return 3;
|
|
106
|
+
}
|
|
107
|
+
const lokalGelesen = leseEineDatei(ordner, verwalteterPfad);
|
|
108
|
+
if (!lokalGelesen.ok) {
|
|
109
|
+
console.error(lokalGelesen.meldung);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
/*
|
|
113
|
+
π΄ **DIE BEIDEN MERKZETTEL GEHEN ALS GANZES HINEIN -- Punkt A-1,
|
|
114
|
+
2026-08-26.** Hier standen vorher drei Zeilen, die den Eintrag, den
|
|
115
|
+
Firmenstand und den Vordruck-Abdruck einzeln aus den beiden Dateien
|
|
116
|
+
herausklaubten. Genau dieser Schritt war die Blindstelle: Der
|
|
117
|
+
Vordruck-Abdruck wurde nie herausgeklaubt, das Argument war optional,
|
|
118
|
+
und deshalb bekam ein Kunde mit einer bestehenden Datei Verbesserungen
|
|
119
|
+
an unserem Fragenkatalog NIE zu sehen -- der Befehl meldete stattdessen
|
|
120
|
+
βDein Firmenkontext ist aktuell".
|
|
121
|
+
|
|
122
|
+
π Die Zuordnung βwas kommt aus welchem Merkzettel" wohnt jetzt in
|
|
123
|
+
`lageAusMerkzetteln` (`kontext-stand.ts`), wo `check:kontext` sie messen
|
|
124
|
+
kann. *Was gesichert sein muss, gehoert auf die reine Seite.*
|
|
125
|
+
*/
|
|
126
|
+
const lage = lageAusMerkzetteln({
|
|
127
|
+
pfad: verwalteterPfad,
|
|
128
|
+
lokalerInhalt: lokalGelesen.inhalt,
|
|
129
|
+
serverInhalt: serverDatei.inhalt,
|
|
130
|
+
lokal: lokalerMerkzettel,
|
|
131
|
+
vomServer: daten.merkzettel,
|
|
132
|
+
});
|
|
133
|
+
if (lage === "beides") {
|
|
134
|
+
// `lokalGelesen.inhalt` ist hier garantiert eine Zeichenkette:
|
|
135
|
+
// `lageFuerDatei` liefert "beides" nie, wenn `lokalerInhalt === null`.
|
|
136
|
+
const sicherung = sichereDatei(ordner, verwalteterPfad, lokalGelesen.inhalt, heute);
|
|
137
|
+
if (!sicherung.ok) {
|
|
138
|
+
console.error(sicherung.meldung);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
// Ruling T8-2: `sichereDatei` ueberschreibt nie und ueberspringt nie --
|
|
142
|
+
// `nebenVorhandener` sagt nur, ob HEUTE schon eine andere Sicherung
|
|
143
|
+
// dieser Datei existiert. Beide Faelle sind echte Erfolge und werden
|
|
144
|
+
// unten bewusst UNTERSCHIEDLICH gemeldet (Β§8).
|
|
145
|
+
gesicherte.push({
|
|
146
|
+
pfad: verwalteterPfad,
|
|
147
|
+
ziel: sicherung.ziel,
|
|
148
|
+
nebenVorhandener: sicherung.nebenVorhandener,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/*
|
|
152
|
+
π΄ **`vorlage-neuer` schreibt wie `server-neuer` -- und sichert bewusst
|
|
153
|
+
NICHT** (Punkt A-1, 2026-08-26). Diese Lage heisst: Die Datei steht noch
|
|
154
|
+
Zeichen fuer Zeichen so da, wie wir sie zuletzt geliefert haben (die
|
|
155
|
+
Pruefsumme im Merkzettel passt), die Firma hat nichts geaendert -- nur
|
|
156
|
+
WIR haben unsere Frage besser formuliert. Es gibt hier also nichts von
|
|
157
|
+
einem Menschen zu verlieren. Haette er die Datei angefasst, waere die
|
|
158
|
+
Lage `beides` und die Sicherung liefe eine Bedingung weiter oben.
|
|
159
|
+
|
|
160
|
+
β οΈ Bis zu diesem Punkt fiel `vorlage-neuer` durch ALLE drei `if`s: nicht
|
|
161
|
+
geschrieben, nicht gesichert, nicht gemeldet. Der Fall konnte bisher nie
|
|
162
|
+
entstehen, weil die Kette dahinter tot war -- ein Zweig, den niemand
|
|
163
|
+
vermisst hat, weil ihn niemand erreichen konnte.
|
|
164
|
+
*/
|
|
165
|
+
if (lage === "server-neuer" || lage === "beides" || lage === "vorlage-neuer") {
|
|
166
|
+
const geschriebenErg = schreibeVerwalteteDatei(ordner, verwalteterPfad, serverDatei.inhalt);
|
|
167
|
+
if (!geschriebenErg.ok) {
|
|
168
|
+
console.error(geschriebenErg.meldung);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
geschrieben.push(verwalteterPfad);
|
|
172
|
+
if (lage === "vorlage-neuer")
|
|
173
|
+
vorlageErneuert.push(verwalteterPfad);
|
|
174
|
+
}
|
|
175
|
+
if (lage === "lokal-geaendert") {
|
|
176
|
+
unberuehrt.push(verwalteterPfad);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/*
|
|
180
|
+
π΄ Der Merkzettel des SERVERS wird geschrieben -- unabhaengig davon, ob in
|
|
181
|
+
diesem Lauf ueberhaupt etwas geaendert wurde. Er traegt fuer JEDE der fuenf
|
|
182
|
+
Dateien den Stand, der zum jetzt ausgelieferten Inhalt passt (Β§7b), auch
|
|
183
|
+
fuer die, die "schon aktuell" waren oder lokal geaendert blieben -- ein
|
|
184
|
+
lokal geaenderter Eintrag bleibt so beim naechsten Lauf ERKENNBAR lokal
|
|
185
|
+
geaendert, statt durch einen fehlenden Merkzettel-Eintrag neu "sicher" zu
|
|
186
|
+
werden.
|
|
187
|
+
|
|
188
|
+
π΄ Task 10a: `firma_id` kommt ausdruecklich aus `daten.firma.id` -- der
|
|
189
|
+
Firma, die diese Antwort tatsaechlich beantwortet hat (`waehleFirma` in
|
|
190
|
+
der Route), nicht aus dem Merkzettel-Feld selbst. Wer zu MEHREREN Firmen
|
|
191
|
+
gehoert, soll im Ordner von Firma A niemals lesen "B hat sich geaendert".
|
|
192
|
+
|
|
193
|
+
π΄ **Punkt A-1: MIT DIESER ZEILE HAELT DER MERKZETTEL DEN NEUEN
|
|
194
|
+
VORLAGEN-FINGERABDRUCK FEST.** `daten.merkzettel.dateien` kommt aus der
|
|
195
|
+
Antwort des Servers und traegt je Datei ihr `vorlage` (`alsMerkzettel`
|
|
196
|
+
verlangt es ausdruecklich). Waere es der LOKALE Merkzettel, staende hier
|
|
197
|
+
weiter der Abdruck von gestern -- und der naechste Lauf meldete denselben
|
|
198
|
+
Unterschied noch einmal, Tag fuer Tag. *Der Teil, den man leicht
|
|
199
|
+
uebersieht: Eine Verbesserung, die jeden Tag neu gemeldet wird, ist
|
|
200
|
+
dieselbe Sorte Fehlalarm wie eine, die nie ankommt.*
|
|
201
|
+
*/
|
|
202
|
+
const merkzettelZumSchreiben = { firma_id: daten.firma.id, dateien: daten.merkzettel.dateien };
|
|
203
|
+
const merkzettelSchreiben = schreibeMerkzettelDatei(ordner, merkzettelZumSchreiben);
|
|
204
|
+
if (!merkzettelSchreiben.ok) {
|
|
205
|
+
console.error(merkzettelSchreiben.meldung);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
// ---- Ausgabe --------------------------------------------------------
|
|
209
|
+
// π΄ Ruling T8-2 (Fix-Runde 1): ZWEI ehrlich verschiedene Faelle, keine
|
|
210
|
+
// Pauschalmeldung mehr. "Frisch gesichert" und "zusaetzlich neben einer
|
|
211
|
+
// schon vorhandenen Sicherung von heute" sind beide echte Erfolge -- aber
|
|
212
|
+
// verschiedene, und der Mensch muss wissen, WAS mit seinem Text passiert
|
|
213
|
+
// ist. Nie ein Satz, der eine Sicherung behauptet, wo keine (mehr)
|
|
214
|
+
// entstanden ist (der urspruengliche Befund).
|
|
215
|
+
//
|
|
216
|
+
// US-7 bleibt erfuellt: Die Meldung steht VOR der "aktualisiert"-Zeile
|
|
217
|
+
// (vor dem Ergebnis) und nennt den Ordner. Die eigentliche Reihenfolge
|
|
218
|
+
// (erst sichern, dann ueberschreiben) erzwingt schon der Code oben.
|
|
219
|
+
if (gesicherte.length > 0) {
|
|
220
|
+
console.log(`Ich sichere deine lokale Fassung, bevor ich sie ersetze -- Ordner: ${sicherungsOrdner(heute)}`);
|
|
221
|
+
for (const g of gesicherte) {
|
|
222
|
+
if (g.nebenVorhandener) {
|
|
223
|
+
const bisherige = sicherungsZiel(heute, g.pfad).split("/").pop();
|
|
224
|
+
const neue = g.ziel.split("/").pop();
|
|
225
|
+
console.log(` β${g.pfad}": Von heute gibt es schon eine Sicherung (${bisherige}) -- die bleibt ` +
|
|
226
|
+
`unveraendert stehen. Deine neuere Fassung sichere ich zusaetzlich als ${neue}, ` +
|
|
227
|
+
"bevor ich die Datei im Ordner ersetze.");
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(` β${g.pfad}" -> ${g.ziel.split("/").pop()}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (geschrieben.length > 0) {
|
|
235
|
+
console.log(`β aktualisiert: ${geschrieben.join(", ")}`);
|
|
236
|
+
}
|
|
237
|
+
// π΄ Punkt A-1: Eine eigene Zeile, kein stilles Mitschreiben. βAktualisiert"
|
|
238
|
+
// heisst fuer den Leser sonst automatisch βmeine Firma hat etwas gesagt" --
|
|
239
|
+
// und dann sucht er in der App eine Aenderung, die es dort nicht gibt.
|
|
240
|
+
if (vorlageErneuert.length > 0) {
|
|
241
|
+
console.log(` Davon aus unserer verbesserten Vorlage: ${vorlageErneuert.join(", ")}`);
|
|
242
|
+
console.log(" Diese Dateien standen noch genau so da, wie wir sie geliefert hatten β es ist nichts\n" +
|
|
243
|
+
" von dir verloren gegangen. HΓ€ttest du darin etwas geΓ€ndert, hΓ€tte ich vorher gesichert.");
|
|
244
|
+
}
|
|
245
|
+
// Β§8: Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall --
|
|
246
|
+
// "nichts geschrieben" heisst hier DREI verschiedene Dinge, und die Ausgabe
|
|
247
|
+
// muss sie unterscheiden, nicht nur die Rueckgabe (alle drei geben 0).
|
|
248
|
+
if (unberuehrt.length > 0) {
|
|
249
|
+
console.log(`Ich fasse nichts an -- du hast lokal geaendert: ${unberuehrt.join(", ")}`);
|
|
250
|
+
// π΄ Der Rat haengt an der DATEI, nicht nur an der Rolle -- s.
|
|
251
|
+
// `ratNachUnberuehrt` (kontext-stand.ts), Review-Befund F-3.
|
|
252
|
+
for (const zeile of ratNachUnberuehrt(unberuehrt, daten.rolle, daten.adminName)) {
|
|
253
|
+
console.log(zeile);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (geschrieben.length === 0 && unberuehrt.length === 0) {
|
|
257
|
+
const alleLeer = VERWALTETE_DATEIEN.every((p) => (daten.merkzettel.dateien[p]?.stand ?? LEER) === LEER);
|
|
258
|
+
if (alleLeer) {
|
|
259
|
+
console.log("Deine Firma hat noch nichts hinterlegt. Bis dahin bleiben die Vordrucke stehen.");
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log("Dein Firmenkontext ist aktuell.");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
/** Prueft die Antwort-Form, ohne `any` -- eine falsche Form ist unlesbar, keine Ausnahme. */
|
|
268
|
+
function alsKontextAntwort(koerper) {
|
|
269
|
+
if (!koerper || typeof koerper !== "object")
|
|
270
|
+
return null;
|
|
271
|
+
const k = koerper;
|
|
272
|
+
// π΄ Task 10a: `firma.id` ist die Quelle, aus der `kontextRunter` gleich
|
|
273
|
+
// den lokalen Merkzettel stempelt -- s. Kommentar an der Schreibstelle.
|
|
274
|
+
const firmaRoh = k.firma;
|
|
275
|
+
if (!firmaRoh || typeof firmaRoh !== "object")
|
|
276
|
+
return null;
|
|
277
|
+
const firmaId = firmaRoh.id;
|
|
278
|
+
if (typeof firmaId !== "string" || firmaId.trim() === "")
|
|
279
|
+
return null;
|
|
280
|
+
const rolle = k.rolle;
|
|
281
|
+
if (rolle !== "admin" && rolle !== "member")
|
|
282
|
+
return null;
|
|
283
|
+
const adminName = k.admin_name;
|
|
284
|
+
if (adminName !== null && typeof adminName !== "string")
|
|
285
|
+
return null;
|
|
286
|
+
const dateienRoh = k.dateien;
|
|
287
|
+
if (!Array.isArray(dateienRoh))
|
|
288
|
+
return null;
|
|
289
|
+
const dateien = [];
|
|
290
|
+
for (const eintrag of dateienRoh) {
|
|
291
|
+
if (!eintrag || typeof eintrag !== "object")
|
|
292
|
+
return null;
|
|
293
|
+
const p = eintrag.pfad;
|
|
294
|
+
const i = eintrag.inhalt;
|
|
295
|
+
if (typeof p !== "string" || p.trim() === "")
|
|
296
|
+
return null;
|
|
297
|
+
if (typeof i !== "string")
|
|
298
|
+
return null;
|
|
299
|
+
dateien.push({ pfad: p, inhalt: i });
|
|
300
|
+
}
|
|
301
|
+
const merkzettel = alsMerkzettel(k.merkzettel);
|
|
302
|
+
if (merkzettel === null)
|
|
303
|
+
return null;
|
|
304
|
+
return { firma: { id: firmaId }, rolle, adminName, dateien, merkzettel };
|
|
305
|
+
}
|
|
@@ -4,22 +4,21 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { STEMPEL_MARKE } from "../nachricht.js";
|
|
6
6
|
import { eigeneVersion } from "../version.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const VERALTETE_BEFEHLE = [];
|
|
7
|
+
export const VERALTETE_BEFEHLE = [
|
|
8
|
+
/*
|
|
9
|
+
π΄ **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
|
|
10
|
+
`/k3analyse`, weil der Befehl Zahlen holt und daraus eine Frage beantwortet
|
|
11
|
+
-- das ist eine Auswertung, und der alte Name sagte nur, WORUEBER sie geht.
|
|
12
|
+
Ohne diese Zeile staenden beide Dateien nebeneinander und taeten dasselbe;
|
|
13
|
+
nur wuerde `k3firma.md` nie wieder gepflegt.
|
|
14
|
+
|
|
15
|
+
β οΈ Damit laeuft `raeumeVeralteteAuf` zum ersten Mal scharf. Der Kommentar
|
|
16
|
+
an der Funktion sagt es selbst: Die Schleife ist nie gelaufen. Deshalb
|
|
17
|
+
gehoert zu diesem Eintrag eine Rot-Probe in BEIDE Richtungen -- eine Datei
|
|
18
|
+
MIT unserer Marke muss verschwinden, eine OHNE muss liegen bleiben.
|
|
19
|
+
*/
|
|
20
|
+
{ alt: "k3firma.md", ersetztDurch: "k3analyse.md" },
|
|
21
|
+
];
|
|
23
22
|
/**
|
|
24
23
|
* Woran wir unsere eigenen Befehlsdateien erkennen.
|
|
25
24
|
*
|
|
@@ -344,7 +343,42 @@ function slashBefehleInstallieren() {
|
|
|
344
343
|
console.error(`Die Slash-Befehle liessen sich nicht alle schreiben: ${String(fehler)}`);
|
|
345
344
|
gestoert = true;
|
|
346
345
|
}
|
|
347
|
-
|
|
346
|
+
/*
|
|
347
|
+
π΄ **NUR aufraeumen, wenn das Schreiben durchlief** (Review-Runde 2,
|
|
348
|
+
2026-08-28). Der Aufruf stand ausserhalb des `try` und lief deshalb auch
|
|
349
|
+
nach einem Fehler -- mit einer boesen Folge: `k3firma.md` wird geloescht,
|
|
350
|
+
obwohl `k3analyse.md` womoeglich nie geschrieben wurde. Der Mensch haette
|
|
351
|
+
danach **keinen von beiden** Befehlen.
|
|
352
|
+
|
|
353
|
+
β οΈ Und der Fall ist naeher, als er klingt: Die Dateien werden alphabetisch
|
|
354
|
+
kopiert, `k3analyse.md` ist die ZWEITE. Ein Rechteproblem oder eine volle
|
|
355
|
+
Platte genau dort genuegt.
|
|
356
|
+
|
|
357
|
+
π Die sichere Richtung ist eindeutig: Lieber traegt jemand den alten
|
|
358
|
+
Befehl eine Runde laenger mit sich herum, als dass er ohne dasteht. Ein
|
|
359
|
+
zweiter Lauf holt beides nach -- der Rueckgabewert sagt ihm das ohnehin.
|
|
360
|
+
*/
|
|
361
|
+
/*
|
|
362
|
+
π΄ **Nachgeschaerft in Runde 3.** Die erste Fassung hiess `if (!gestoert)`
|
|
363
|
+
und deckte nur EINE der drei Arten ab, auf die eine Datei nicht landet:
|
|
364
|
+
`gestoert` (eine Ausnahme beim Kopieren) -- aber nicht `unlesbar` und nicht
|
|
365
|
+
`uebersprungen` (dort liegt eine Datei OHNE unsere Marke, die wir bewusst
|
|
366
|
+
stehen lassen). In beiden Faellen waere `k3firma.md` geloescht worden,
|
|
367
|
+
obwohl `k3analyse.md` nie geschrieben wurde.
|
|
368
|
+
|
|
369
|
+
π Die Bedingung trifft jetzt die Absicht direkt: **Ein alter Name wird nur
|
|
370
|
+
entfernt, wenn sein Ersatz wirklich liegt.** Steht kein Ersatz in der
|
|
371
|
+
Tabelle, gilt der Eintrag als ersatzlos gestrichen und darf immer weg.
|
|
372
|
+
*/
|
|
373
|
+
/*
|
|
374
|
+
β οΈ **Beide Schreibweisen, genau wie `raeumeVeralteteAuf`** (Review-Runde 4).
|
|
375
|
+
`VERALTETE_BEFEHLE` nimmt `"k3alt"` UND `"k3alt.md"`; `geschrieben` traegt
|
|
376
|
+
immer den Dateinamen. Ohne die Angleichung schluege der Vergleich fehl, und
|
|
377
|
+
ein Eintrag ohne `.md` bliebe fuer immer liegen.
|
|
378
|
+
*/
|
|
379
|
+
const alsDatei = (n) => (n.endsWith(".md") ? n : `${n}.md`);
|
|
380
|
+
const ersatzLiegt = VERALTETE_BEFEHLE.filter((e) => e.ersetztDurch === null || geschrieben.includes(alsDatei(e.ersetztDurch))).map((e) => e.alt);
|
|
381
|
+
raeumeVeralteteAuf(zielOrdner, ersatzLiegt);
|
|
348
382
|
return {
|
|
349
383
|
geschrieben: geschrieben.map((n) => n.replace(/\.md$/, "")),
|
|
350
384
|
uebersprungen: uebersprungen.map((n) => n.replace(/\.md$/, "")),
|
package/dist/befehle/werkbank.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { dateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
3
|
+
import { FIRMEN_OPTIONEN, dateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
4
|
+
import { alsBefehlszeile, alsWort, trenneNamenUndOptionen, zerlege } from "../argumente.js";
|
|
4
5
|
import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
|
|
6
|
+
import { FIRMEN_CLAUDE_MD, MERKZETTEL_PFAD, firmenAbgleich, firmennameAusClaudeMd, leseMerkzettel, } from "../kontext-stand.js";
|
|
7
|
+
import { leseEineDatei, leseMerkzettelDatei } from "../kontext-dateien.js";
|
|
5
8
|
/**
|
|
6
9
|
* Legt **nur eine Werkbank** an β in einem Firmen-Ordner, der schon steht.
|
|
7
10
|
*
|
|
@@ -54,7 +57,7 @@ import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
|
|
|
54
57
|
* darf ein lokaler Befehl nicht ΓΌberschreiben.
|
|
55
58
|
*/
|
|
56
59
|
const HILFE = `
|
|
57
|
-
kimal3 werkbank neu [dein-vorname]
|
|
60
|
+
kimal3 werkbank neu [dein-vorname] [--firma "Name" | --firma-id "β¦"]
|
|
58
61
|
|
|
59
62
|
Legt eine eigene Werkbank in einem bestehenden Firmen-Ordner an.
|
|
60
63
|
Die Firmenebene (kontext/, CLAUDE.md, README.md) wird NICHT angefasst.
|
|
@@ -63,6 +66,12 @@ const HILFE = `
|
|
|
63
66
|
wird "werkbank-sabine". Ohne Angabe steht dort der Name,
|
|
64
67
|
den der Server aus deiner Anmelde-Adresse gebildet hat.
|
|
65
68
|
|
|
69
|
+
--firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
|
|
70
|
+
Die Absage nennt dir die Namen als fertige Befehle.
|
|
71
|
+
|
|
72
|
+
--firma-id "β¦" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
|
|
73
|
+
Auch diese Absage nennt dir fertige Befehle.
|
|
74
|
+
|
|
66
75
|
π΄ Bricht ab, wenn der Zielordner schon existiert und NICHT leer ist.
|
|
67
76
|
|
|
68
77
|
Du hast noch gar keinen Firmen-Ordner?
|
|
@@ -94,7 +103,7 @@ const HILFE = `
|
|
|
94
103
|
* Eingestaendnis, dass man es nicht weiss -- hier weiss man es.*
|
|
95
104
|
*/
|
|
96
105
|
function hinweisFuer(ordnerName) {
|
|
97
|
-
const ziel = ordnerName
|
|
106
|
+
const ziel = alsWort(ordnerName);
|
|
98
107
|
return (" Ist das DEINE Werkbank? Dann steht sie schon -- geh einfach hinein:\n" +
|
|
99
108
|
` cd ${ziel}\n` +
|
|
100
109
|
" Bist du es nicht, nimm deinen eigenen Vornamen:\n" +
|
|
@@ -116,37 +125,58 @@ export async function werkbankNeu(argv) {
|
|
|
116
125
|
`werkbank-werkbank-sabine` zu bauen. *Alte Anleitungen ueberleben laenger
|
|
117
126
|
als der Code, den sie beschreiben.*
|
|
118
127
|
*/
|
|
119
|
-
|
|
128
|
+
/*
|
|
129
|
+
π΄ **Erst schneiden, dann zerlegen** -- wortgleich zum Zwilling in
|
|
130
|
+
`arbeitsplatz.ts` (Punkt A-3, 2026-08-26). Dieser Befehl hat ein Wort OHNE
|
|
131
|
+
`--` (den Vornamen), und `zerlege` lehnt genau das ab.
|
|
132
|
+
*/
|
|
133
|
+
const { woerter, optionen: optionsArgv } = trenneNamenUndOptionen(argv);
|
|
134
|
+
const nameArg = woerter[0];
|
|
120
135
|
if (nameArg !== undefined && nameArg.trim() === "") {
|
|
121
136
|
console.error(`Ein leerer Name ist kein gueltiger Name.\n${HILFE}`);
|
|
122
137
|
return 1;
|
|
123
138
|
}
|
|
124
|
-
if (
|
|
125
|
-
console.error(`Die Option β${nameArg}" kenne ich nicht.\n${HILFE}`);
|
|
126
|
-
return 1;
|
|
127
|
-
}
|
|
128
|
-
if (argv.length > 1) {
|
|
139
|
+
if (woerter.length > 1) {
|
|
129
140
|
/*
|
|
130
141
|
π΄ **Der Vorschlag darf keine Optionen einsammeln** (Review-Runde 5 zu
|
|
131
142
|
PR #25). Hier stand `argv.join("-")` -- aus `werkbank neu sabine --force`
|
|
132
|
-
wurde damit der Rat `werkbank neu sabine---force`.
|
|
133
|
-
Zeilen hoeher sieht nur `argv[0]`.
|
|
143
|
+
wurde damit der Rat `werkbank neu sabine---force`.
|
|
134
144
|
*Ein Rat, der ein falsches Kommando erzeugt, ist schlimmer als keiner* --
|
|
135
145
|
dieselbe Klasse wie die Absagen aus Runde 1 und 2, nur kleiner.
|
|
136
146
|
|
|
137
147
|
Deshalb: Sieht irgendein Wort wie eine Option aus, gibt es gar keinen
|
|
138
148
|
Vorschlag. Ein Mensch, der `--force` tippt, sucht keine Schreibhilfe fuer
|
|
139
149
|
seinen Vornamen.
|
|
150
|
+
|
|
151
|
+
β οΈ Seit dem 2026-08-26 stehen die `--`-Optionen schon gar nicht mehr in
|
|
152
|
+
`woerter` (der Schnitt oben). Der Filter bleibt trotzdem: Er faengt das
|
|
153
|
+
EINFACHE `-` (`-force`), das kein Schnitt-Zeichen ist.
|
|
140
154
|
*/
|
|
141
|
-
const
|
|
142
|
-
|
|
155
|
+
const einfacheWoerter = woerter.filter((w) => !w.startsWith("-"));
|
|
156
|
+
/*
|
|
157
|
+
π΄ **Die getippten Optionen reisen mit** (Review-Befund 3, 2026-08-26).
|
|
158
|
+
Ohne sie riet die Zeile zu einem Befehl, der bei einem Menschen in zwei
|
|
159
|
+
Firmen sofort wieder in dieselbe Absage laeuft -- s. `alsBefehlszeile`.
|
|
160
|
+
*/
|
|
161
|
+
const optionsAnhang = optionsArgv.length === 0 ? "" : ` ${alsBefehlszeile(optionsArgv)}`;
|
|
162
|
+
const vorschlag = einfacheWoerter.length === woerter.length
|
|
143
163
|
? `Besteht dein Name aus mehreren Woertern? Dann schreib ihn zusammen:\n` +
|
|
144
|
-
` kimal3 werkbank neu ${
|
|
164
|
+
` kimal3 werkbank neu ${einfacheWoerter.join("-")}${optionsAnhang}\n`
|
|
145
165
|
: "";
|
|
146
|
-
console.error(`Mit β${
|
|
166
|
+
console.error(`Mit β${woerter[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
|
|
147
167
|
`${vorschlag}${HILFE}`);
|
|
148
168
|
return 1;
|
|
149
169
|
}
|
|
170
|
+
// π Erst NACH den Namens-Absagen -- s. Zwilling in `arbeitsplatz.ts`.
|
|
171
|
+
const zerlegt = zerlege(optionsArgv, FIRMEN_OPTIONEN);
|
|
172
|
+
if (!zerlegt.ok) {
|
|
173
|
+
console.error(`${zerlegt.meldung}\n${HILFE}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const wunsch = {
|
|
177
|
+
firma: zerlegt.werte.optionen.firma,
|
|
178
|
+
firmaId: zerlegt.werte.optionen["firma-id"],
|
|
179
|
+
};
|
|
150
180
|
// `null` heisst: nach dem Umschreiben blieb kein Zeichen uebrig, das in einem
|
|
151
181
|
// Ordnernamen stehen darf. Ein Mensch hat hier getippt und verdient eine
|
|
152
182
|
// Antwort -- der Server schweigt im selben Fall und bleibt beim Vordruck.
|
|
@@ -161,7 +191,17 @@ export async function werkbankNeu(argv) {
|
|
|
161
191
|
return 1;
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
|
-
|
|
194
|
+
/*
|
|
195
|
+
π΄ **HIER ENTSCHEIDET SICH, OB UEBERHAUPT ETWAS ENTSTEHT** (Punkt A-3) --
|
|
196
|
+
dieselbe Stelle wie beim Zwilling: Bei mehreren Firmen ohne Angabe kommt
|
|
197
|
+
`409`, dieser Aufruf endet mit `1`, und `dateienSchreiben` weiter unten
|
|
198
|
+
wird nie erreicht.
|
|
199
|
+
*/
|
|
200
|
+
// π Der getippte Vorname reist in der Absage mit -- s. Zwilling.
|
|
201
|
+
const befehlsname = nameArg === undefined
|
|
202
|
+
? "werkbank neu"
|
|
203
|
+
: `werkbank neu ${alsWort(nameArg)}`;
|
|
204
|
+
const holen = await vorlageHolen(befehlsname, wunsch);
|
|
165
205
|
if (!holen.ok)
|
|
166
206
|
return holen.wert;
|
|
167
207
|
const daten = holen.daten;
|
|
@@ -305,6 +345,77 @@ export async function werkbankNeu(argv) {
|
|
|
305
345
|
console.error(" verschiedene Firmenordner mit verschiedenen Regeln darin.");
|
|
306
346
|
return 1;
|
|
307
347
|
}
|
|
348
|
+
/*
|
|
349
|
+
π΄ **STEHT DIE GELIEFERTE FIRMA UEBERHAUPT HIER?** (Befund 1 der zweiten
|
|
350
|
+
Review-Runde, 2026-08-27.)
|
|
351
|
+
|
|
352
|
+
Bis hierher ist geprueft, dass NEBEN uns eine Firmenebene liegt -- aber
|
|
353
|
+
nicht, ob es DIE Firma ist, deren Namen die Lieferung gleich in die
|
|
354
|
+
`werkbank-<name>/CLAUDE.md` schreibt. Seit A-11 ist das keine Kosmetik
|
|
355
|
+
mehr: Dort stand frueher fuer jede Firma derselbe Platzhalter, jetzt steht
|
|
356
|
+
dort ein Name, den Claude bei jedem Start liest. Die ganze Fallgeschichte
|
|
357
|
+
haengt an `firmenAbgleich` in `../kontext-stand.js`.
|
|
358
|
+
|
|
359
|
+
π **Der Vergleich laeuft ueber die IDs im Merkzettel** -- der des Ordners,
|
|
360
|
+
in dem wir stehen, gegen den, der gerade geliefert wurde. Beide Seiten
|
|
361
|
+
kennen ihre Firma aus derselben Quelle, es wird nichts geraten.
|
|
362
|
+
|
|
363
|
+
π΄ **UND ER STEHT VOR `dateienSchreiben`.** Ein Abbruch hier hinterlaesst
|
|
364
|
+
keine halbe Werkbank -- dieselbe Haltung wie in `kontextHoch`
|
|
365
|
+
(βich schicke nichts Halbes hochβ).
|
|
366
|
+
*/
|
|
367
|
+
const inhaltAus = (pfad) => daten.dateien.find((d) => d.pfad === pfad)?.inhalt ?? null;
|
|
368
|
+
const hierGelesen = leseEineDatei(process.cwd(), FIRMEN_CLAUDE_MD);
|
|
369
|
+
const abgleich = firmenAbgleich({
|
|
370
|
+
id: leseMerkzettelDatei(process.cwd()).firma_id,
|
|
371
|
+
name: firmennameAusClaudeMd(hierGelesen.ok ? hierGelesen.inhalt : null),
|
|
372
|
+
}, {
|
|
373
|
+
id: leseMerkzettel(inhaltAus(MERKZETTEL_PFAD)).firma_id,
|
|
374
|
+
name: firmennameAusClaudeMd(inhaltAus(FIRMEN_CLAUDE_MD)),
|
|
375
|
+
});
|
|
376
|
+
/*
|
|
377
|
+
π΄ **DER VERDACHT WARNT, ER SPERRT NICHT** (Befund 1 der vierten
|
|
378
|
+
Review-Runde). Steht der Name nur deshalb anders da, weil jemand die
|
|
379
|
+
`CLAUDE.md` seiner Firma bearbeitet hat -- was er ausdruecklich darf --,
|
|
380
|
+
waere eine Absage falsch UND ausweglos. Die Zeile hier sagt, was sie
|
|
381
|
+
sieht, und laesst den Menschen entscheiden.
|
|
382
|
+
*/
|
|
383
|
+
if (abgleich.art === "zweifel") {
|
|
384
|
+
console.error(`Achtung: Dieser Ordner nennt als Firma β${abgleich.hier}",`);
|
|
385
|
+
console.error(`du hast aber β${abgleich.gewaehlt}" gewaehlt.`);
|
|
386
|
+
console.error("Ich lege die Werkbank trotzdem an -- vielleicht heisst die Firma bei euch");
|
|
387
|
+
console.error("nur anders geschrieben. Stimmt es nicht, loesch den neuen Ordner wieder.");
|
|
388
|
+
console.error("");
|
|
389
|
+
}
|
|
390
|
+
if (abgleich.art === "andere-firma") {
|
|
391
|
+
/*
|
|
392
|
+
π΄ **ZWEI LAGEN, ZWEI SAETZE** (Befund 3 der dritten Review-Runde). Hier
|
|
393
|
+
stand ein Rueckfall `?? "die genannte Firma"`, und der war erreichbar:
|
|
394
|
+
Wer zu genau EINER Firma gehoert, tippt gar kein `--firma`. Steht er im
|
|
395
|
+
Ordner einer anderen Firma, las er woertlich *βDieser Ordner gehoert zu
|
|
396
|
+
einer ANDEREN Firma als βdie genannte Firma'."* -- ein Satz, der nichts
|
|
397
|
+
benennt und wie ein Fehler im Programm aussieht.
|
|
398
|
+
*Ein Platzhalter in einer Meldung ist ein Satz, den niemand geschrieben
|
|
399
|
+
hat.*
|
|
400
|
+
*/
|
|
401
|
+
const genannt = wunsch.firma ?? wunsch.firmaId;
|
|
402
|
+
if (genannt === undefined) {
|
|
403
|
+
console.error("Dieser Ordner gehoert zu einer anderen Firma als der, zu der du gehoerst.");
|
|
404
|
+
console.error("Ich lege hier keine Werkbank an, die einen fremden Firmennamen traegt --");
|
|
405
|
+
console.error("sie wuerde Claude bei jedem Start die falsche Firma nennen.");
|
|
406
|
+
console.error("");
|
|
407
|
+
console.error(" Deine Werkbank gehoert in den Ordner DEINER Firma. Geh dorthin und");
|
|
408
|
+
console.error(" versuch es dort noch einmal.");
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
console.error(`Dieser Ordner gehoert zu einer ANDEREN Firma als β${genannt}".`);
|
|
412
|
+
console.error("Ich lege hier keine Werkbank an, die einen fremden Firmennamen traegt --");
|
|
413
|
+
console.error("sie wuerde Claude bei jedem Start die falsche Firma nennen.");
|
|
414
|
+
console.error("");
|
|
415
|
+
console.error(` Soll die Werkbank fuer β${genannt}" sein? Dann gehoert sie in DEREN Ordner.`);
|
|
416
|
+
console.error(" Soll sie fuer die Firma hier sein? Dann nimm deren Namen.");
|
|
417
|
+
return 1;
|
|
418
|
+
}
|
|
308
419
|
/*
|
|
309
420
|
π Der Zielordner ist der Werkbank-Name -- entweder der, den der Server
|
|
310
421
|
schon gesetzt hat (`werkbank`, aus der Anmelde-Adresse gebildet), oder der
|
|
@@ -351,7 +462,7 @@ export async function werkbankNeu(argv) {
|
|
|
351
462
|
console.log(" Die Firmenebene ist unberuehrt geblieben.");
|
|
352
463
|
console.log("");
|
|
353
464
|
console.log(" Weiter geht's:");
|
|
354
|
-
console.log(` cd ${ordnerName
|
|
465
|
+
console.log(` cd ${alsWort(ordnerName)}`);
|
|
355
466
|
console.log(" claude");
|
|
356
467
|
console.log(' dann "Lass uns einrichten" tippen');
|
|
357
468
|
return 0;
|