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.
- package/README.md +19 -0
- package/dist/api.js +61 -1
- package/dist/arbeitsplatz-dateien.js +268 -0
- package/dist/befehle/arbeitsplatz.js +260 -0
- package/dist/befehle/skill-install.js +110 -4
- package/dist/befehle/werkbank.js +389 -0
- package/dist/befehle-katalog.js +59 -0
- package/dist/gesehen.js +85 -0
- package/dist/kimal3.js +172 -13
- package/dist/nachricht.js +198 -0
- package/dist/version.js +37 -0
- package/dist/werkbank-name.js +217 -0
- package/package.json +1 -1
- package/skill/SKILL.md +212 -125
package/dist/gesehen.js
ADDED
|
@@ -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
|
+
}
|
package/dist/kimal3.js
CHANGED
|
@@ -11,16 +11,21 @@
|
|
|
11
11
|
* `kimal3 login`, bei 4 hilft genau das nicht. Wer beide auf dieselbe Zahl
|
|
12
12
|
* legt, laesst Claude den falschen Rat geben.
|
|
13
13
|
*/
|
|
14
|
-
import { readFileSync } from "node:fs";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
15
|
import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
|
|
16
|
-
import { NichtAngemeldet } from "./api.js";
|
|
16
|
+
import { gemerkterHinweis, NichtAngemeldet } from "./api.js";
|
|
17
|
+
import { leseGesehen, merkeGesehen } from "./gesehen.js";
|
|
18
|
+
import { formatiere, heuteAus, skillMeldung, skillZustand, sollZeigen, } from "./nachricht.js";
|
|
19
|
+
import { eigeneVersion } from "./version.js";
|
|
17
20
|
import { hilfeText } from "./befehle-katalog.js";
|
|
21
|
+
import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
|
|
22
|
+
import { werkbankNeu } from "./befehle/werkbank.js";
|
|
18
23
|
import { firmaDaten } from "./befehle/firma-daten.js";
|
|
19
24
|
import { login } from "./befehle/login.js";
|
|
20
25
|
import { logout } from "./befehle/logout.js";
|
|
21
26
|
import { projektListe } from "./befehle/projekt-liste.js";
|
|
22
27
|
import { projektNeu } from "./befehle/projekt-neu.js";
|
|
23
|
-
import { skillInstall } from "./befehle/skill-install.js";
|
|
28
|
+
import { skillDatei, skillInstall } from "./befehle/skill-install.js";
|
|
24
29
|
// 📌 Kein handgeschriebener Block mehr (2026-08-18): Die Liste wohnt in
|
|
25
30
|
// `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
|
|
26
31
|
// Zwei Listen ueber dieselben Befehle driften — die eine bekaeme den naechsten
|
|
@@ -56,20 +61,167 @@ const HILFE = hilfeText();
|
|
|
56
61
|
* eine Ebene unter `cli/`.
|
|
57
62
|
*/
|
|
58
63
|
function versionsZeilen() {
|
|
59
|
-
|
|
64
|
+
// 📌 Seit dem 2026-08-21 wird die Nummer NICHT mehr hier gelesen, sondern in
|
|
65
|
+
// `version.ts` -- sie hat einen zweiten Aufrufer bekommen (den Skill-Stempel
|
|
66
|
+
// in `skill install`). Zwei Lesestellen fuer dieselbe Zahl waeren genau das
|
|
67
|
+
// Muster, gegen das der Kommentar oben schon einmal geschrieben wurde.
|
|
68
|
+
//
|
|
69
|
+
// "unbekannt" bleibt hier stehen und wandert NICHT in `version.ts`: Diese
|
|
70
|
+
// Zeile zeigt einem Menschen etwas an, und da ist "unbekannt" die ehrliche
|
|
71
|
+
// Antwort. Der andere Aufrufer RECHNET mit dem Wert und braucht dort das
|
|
72
|
+
// Gegenteil -- er will lieber gar keine Nummer als eine erfundene.
|
|
73
|
+
const version = eigeneVersion() ?? "unbekannt";
|
|
74
|
+
return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Welcher Update-Hinweis soll ans Ende -- oder keiner?
|
|
78
|
+
*
|
|
79
|
+
* 🔑 WARUM stderr (entschieden wird es erst beim Schreiben, gemeint ist es
|
|
80
|
+
* hier): Dort stoert der Hinweis keine Weiterverarbeitung --
|
|
81
|
+
* `kimal3 firma daten > firma.json` bleibt sauberes JSON --, und Claude sieht
|
|
82
|
+
* ihn trotzdem, denn es liest beide Kanaele.
|
|
83
|
+
*
|
|
84
|
+
* 🔴 WARUM ALLES IN EINEM try STECKT: Diese Funktion laeuft NACH dem
|
|
85
|
+
* eigentlichen Befehl. Wenn sie wirft, faellt ein bereits erfolgreicher Befehl
|
|
86
|
+
* auf Rueckgabewert 3 -- und Claude gaebe den Rat "gleich noch einmal" fuer
|
|
87
|
+
* etwas, das laengst erledigt ist. Die Zahlen sind laut dem Kopf dieser Datei
|
|
88
|
+
* die eigentliche Schnittstelle; ein Nebenschauplatz darf sie nicht anfassen.
|
|
89
|
+
*/
|
|
90
|
+
function hinweisText() {
|
|
60
91
|
try {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
const eigene = eigeneVersion();
|
|
93
|
+
// Ohne eigene Nummer ist jeder Vergleich geraten -- dann lieber schweigen.
|
|
94
|
+
// Das ist der Fall "package.json unlesbar", und da hat der Kunde groessere
|
|
95
|
+
// Sorgen als einen Update-Hinweis.
|
|
96
|
+
if (eigene === null)
|
|
97
|
+
return null;
|
|
98
|
+
const heute = heuteAus(new Date());
|
|
99
|
+
const h = gemerkterHinweis();
|
|
100
|
+
if (h && sollZeigen(h, eigene, leseGesehen(), heute)) {
|
|
101
|
+
// Hoechstens EIN Hinweis je Aufruf -- zwei sind eine Wand.
|
|
102
|
+
return { text: formatiere(h), kennung: h.id, tag: heute };
|
|
103
|
+
}
|
|
104
|
+
/*
|
|
105
|
+
Weg 2: ohne Netz.
|
|
106
|
+
⚠️ Er greift immer dann, wenn oben NICHTS gezeigt wird -- nicht nur,
|
|
107
|
+
wenn gar kein Netz-Hinweis ankam (praezisiert nach der dritten
|
|
108
|
+
Review-Runde). Auch "die Nachricht stand heute schon da" und "sie gilt
|
|
109
|
+
fuer diese Fassung nicht" fuehren hierher. Das ist gewollt: Es bleibt
|
|
110
|
+
bei einem Hinweis je Aufruf.
|
|
111
|
+
*Hier stand vorher "nur, wenn kein Netz-Hinweis kam" -- eine Bedingung,
|
|
112
|
+
die der Code nicht hat. Genau der Kommentartyp, den dieses Projekt schon
|
|
113
|
+
zweimal teuer bezahlt hat.*
|
|
114
|
+
*/
|
|
115
|
+
// Der Pfad kommt aus `skill-install.ts` -- dort wird er geschrieben, hier
|
|
116
|
+
// gelesen. Zwei Zusammenbauten desselben Ortes driften, und der zweite
|
|
117
|
+
// verrottet lautlos (Befund der vierten Review-Runde).
|
|
118
|
+
const datei = skillDatei();
|
|
119
|
+
const inhalt = existsSync(datei) ? readFileSync(datei, "utf8") : null;
|
|
120
|
+
if (skillZustand(inhalt, eigene) === "veraltet") {
|
|
121
|
+
// Auch dieser Hinweis nur einmal am Tag, unter einer festen Kennung.
|
|
122
|
+
// Sie kann mit keiner Nachrichten-UUID kollidieren.
|
|
123
|
+
const gesehen = leseGesehen();
|
|
124
|
+
if (gesehen["skill-veraltet"] !== heute) {
|
|
125
|
+
return { text: skillMeldung(), kennung: "skill-veraltet", tag: heute };
|
|
126
|
+
}
|
|
66
127
|
}
|
|
67
128
|
}
|
|
68
129
|
catch {
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
/* Ein Nebenschauplatz darf den Befehl nicht umwerfen. */
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Den Hinweis schreiben und ERST DANN aussteigen.
|
|
136
|
+
*
|
|
137
|
+
* 🔴 WARUM NICHT EINFACH `console.error` UND `process.exit`: Auf Windows ist
|
|
138
|
+
* `process.stderr` auch bei Roehren und Terminals ASYNCHRON, und
|
|
139
|
+
* `process.exit()` wirft ausstehende Schreibvorgaenge weg (Befund der ersten
|
|
140
|
+
* Review-Runde). Der Hinweis ist das Letzte, was geschrieben wird -- er waere
|
|
141
|
+
* damit genau das Erste, was verlorengeht. *Ein Merkmal, dessen ganzer Wert an
|
|
142
|
+
* der letzten Zeile haengt, darf diese Zeile nicht dem Zufall ueberlassen.*
|
|
143
|
+
*
|
|
144
|
+
* `process.exitCode` wird vorher gesetzt: Sollte der Rueckruf wider Erwarten
|
|
145
|
+
* ausbleiben und der Prozess von selbst enden, stimmt die Zahl trotzdem -- und
|
|
146
|
+
* die Zahl ist laut dem Kopf dieser Datei die eigentliche Schnittstelle.
|
|
147
|
+
*/
|
|
148
|
+
function endeMitHinweis(code) {
|
|
149
|
+
try {
|
|
150
|
+
process.exitCode = code;
|
|
151
|
+
/*
|
|
152
|
+
🔴 NUR NACH EINEM GELUNGENEN BEFEHL (Befund der sechsten Review-Runde).
|
|
153
|
+
`haupt()` WIRFT bei den meisten Fehlern nicht, sondern gibt eine Zahl
|
|
154
|
+
zurueck -- 1 fuer einen unbekannten Befehl, 2 und 4 aus den Befehlen
|
|
155
|
+
selbst. Der Hinweis landete damit unter Fehlermeldungen, obwohl der
|
|
156
|
+
`.catch`-Zweig unten und der Wrapper auf dem Server beide das Gegenteil
|
|
157
|
+
begruenden: "Eine Fehlermeldung ist der schlechteste Augenblick fuer eine
|
|
158
|
+
Nebenbemerkung."
|
|
159
|
+
|
|
160
|
+
⚠️ Schlimmer als die Optik ist die Nebenwirkung: Der gescheiterte Lauf
|
|
161
|
+
verbrauchte den EINEN Platz des Tages. Wer sich vertippt und danach
|
|
162
|
+
richtig tippt, saehe die Nachricht dann heute gar nicht mehr.
|
|
163
|
+
*/
|
|
164
|
+
const raus = code === 0 ? hinweisText() : null;
|
|
165
|
+
/*
|
|
166
|
+
🔴 BEIDE KANAELE LEEREN, NICHT NUR STDERR. Die erste Fassung dieser
|
|
167
|
+
Funktion sicherte stderr -- und liess den haeufigeren Weg (kein Hinweis)
|
|
168
|
+
bei einem nackten `process.exit` stehen. Genau der Fehler, gegen den sie
|
|
169
|
+
geschrieben war, stand also noch daneben.
|
|
170
|
+
|
|
171
|
+
Es trifft naemlich vor allem STDOUT: `firma daten` schreibt ein
|
|
172
|
+
mehrere Kilobyte grosses JSON mit `console.log` und landet dann hier.
|
|
173
|
+
Auf Windows sind beide Kanaele asynchron; `process.exit()` wirft
|
|
174
|
+
Ausstehendes weg. Das Ergebnis waere das schlimmstmoegliche: ein
|
|
175
|
+
abgeschnittenes JSON MIT Rueckgabewert 0 -- Claude liest es als
|
|
176
|
+
gelungene Auskunft.
|
|
177
|
+
|
|
178
|
+
🔑 Warum ueberhaupt noch `process.exit` und kein natuerliches Ende:
|
|
179
|
+
Nach einem `fetch` haelt Node seine Verbindungen eine Weile offen. Der
|
|
180
|
+
Befehl wuerde sekundenlang scheinbar haengen, nachdem er fertig ist.
|
|
181
|
+
|
|
182
|
+
Die Notbremse ist `unref`t: Sie haelt die Schleife nicht am Leben,
|
|
183
|
+
feuert aber, falls ein Kanal wirklich haengt. Und weil `exitCode` schon
|
|
184
|
+
oben steht, stimmt die Zahl in JEDEM dieser Wege -- auch dann, wenn der
|
|
185
|
+
Prozess von selbst endet.
|
|
186
|
+
|
|
187
|
+
🔴 UND DIE REIHENFOLGE IST DER PUNKT (Befund der sechsten Review-Runde):
|
|
188
|
+
ERST stdout leeren, DANN den Hinweis auf stderr schreiben. Die beiden
|
|
189
|
+
Kanaele haben eigene Puffer, und ihre Reihenfolge ist nur dann sicher,
|
|
190
|
+
wenn beide synchron sind -- auf einer POSIX-Roehre ist stdout es NICHT.
|
|
191
|
+
Bei `kimal3 firma daten 2>&1 | tee firma.json` mit mehr als 64 KiB
|
|
192
|
+
Nutzlast koennte die kleine stderr-Zeile also VOR dem grossen JSON
|
|
193
|
+
ankommen und mitten hineinrutschen. Genau das verbietet US-1 ("nie
|
|
194
|
+
mittendrin"), und es waere eine Ausgabe, die SKILL.md als Firmendaten
|
|
195
|
+
zu lesen anweist.
|
|
196
|
+
📌 Auf Windows liess es sich nicht herstellen -- dort sind Roehren
|
|
197
|
+
synchron. *Eine Reihenfolge, die nur auf dem Rechner haelt, auf dem man
|
|
198
|
+
sie gemessen hat, ist keine Reihenfolge, sondern ein Zufall.*
|
|
199
|
+
*/
|
|
200
|
+
setTimeout(() => process.exit(code), 2000).unref();
|
|
201
|
+
process.stdout.write("", () => {
|
|
202
|
+
if (!raus) {
|
|
203
|
+
process.stderr.write("", () => process.exit(code));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Erst schreiben, DANN vermerken -- und nur, wenn kein Fehler zurueckkam.
|
|
207
|
+
process.stderr.write(`${raus.text}\n`, (fehler) => {
|
|
208
|
+
if (!fehler)
|
|
209
|
+
merkeGesehen(raus.kennung, raus.tag);
|
|
210
|
+
process.exit(code);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/*
|
|
216
|
+
⚠️ Auch das Schreiben selbst kann werfen -- ein geschlossener
|
|
217
|
+
Ausgabekanal genuegt. Ohne dieses `catch` liefe der Wurf in den
|
|
218
|
+
`.catch`-Zweig unten, und der macht aus einem GELUNGENEN Befehl eine
|
|
219
|
+
3 ("gleich noch einmal"). Das ist genau das, was der Kommentar an
|
|
220
|
+
`hinweisText` verspricht, dass es nie passiert -- versprochen war es
|
|
221
|
+
dort aber nur fuer die halbe Strecke.
|
|
222
|
+
*/
|
|
223
|
+
process.exit(code);
|
|
71
224
|
}
|
|
72
|
-
return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
|
|
73
225
|
}
|
|
74
226
|
/**
|
|
75
227
|
* Was diese Zeile sagen KANN — und was nicht.
|
|
@@ -123,11 +275,18 @@ async function haupt() {
|
|
|
123
275
|
return firmaDaten(argv.slice(2));
|
|
124
276
|
if (wort === "skill install")
|
|
125
277
|
return skillInstall();
|
|
278
|
+
if (argv[0] === "arbeitsplatz" && argv[1] === "neu")
|
|
279
|
+
return arbeitsplatzNeu(argv.slice(2));
|
|
280
|
+
if (argv[0] === "werkbank" && argv[1] === "neu")
|
|
281
|
+
return werkbankNeu(argv.slice(2));
|
|
126
282
|
console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
|
|
127
283
|
return 1;
|
|
128
284
|
}
|
|
129
285
|
haupt()
|
|
130
|
-
.then((code) =>
|
|
286
|
+
.then((code) => endeMitHinweis(code))
|
|
287
|
+
// ⚠️ Im Fehlerzweig bewusst KEIN Hinweis. Wer gerade eine Fehlermeldung
|
|
288
|
+
// liest, braucht keine zweite Zeile ueber Updates -- und ein Netz-Hinweis
|
|
289
|
+
// kann hier ohnehin nicht angekommen sein, denn genau das Netz hat versagt.
|
|
131
290
|
.catch((f) => {
|
|
132
291
|
if (f instanceof NichtAngemeldet) {
|
|
133
292
|
console.error("Bitte zuerst `kimal3 login` ausführen.");
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
}
|
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
|
+
}
|