kimal3 0.1.1 → 0.1.2
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/dist/api.js +6 -9
- package/dist/argumente.js +35 -7
- package/dist/befehle/firma-daten.js +113 -0
- package/dist/befehle/projekt-liste.js +90 -9
- package/dist/befehle/projekt-neu.js +8 -2
- package/dist/befehle/skill-install.js +271 -6
- package/dist/befehle-katalog.js +112 -0
- package/dist/fehler.js +66 -0
- package/dist/firmen-liste.js +52 -0
- package/dist/kimal3.js +51 -12
- package/dist/projekt-zeilen.js +253 -0
- package/package.json +27 -16
- package/skill/SKILL.md +125 -43
- package/skill/commands/k3.md +44 -0
- package/skill/commands/k3firma.md +57 -0
- package/skill/commands/k3liste.md +26 -0
- package/skill/commands/k3projekt.md +44 -0
package/dist/api.js
CHANGED
|
@@ -40,12 +40,9 @@ export function feld(koerper, name) {
|
|
|
40
40
|
}
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
43
|
+
// `textListe` stand hier bis zum 2026-08-18 und zog EIN Textfeld je Eintrag --
|
|
44
|
+
// genau das war die Grenze, an der `projekt liste` haengen blieb (Befund L-4).
|
|
45
|
+
// Die Liste zeigt jetzt mehrere Felder je Projekt und liest ihre Antwort in
|
|
46
|
+
// `projekt-zeilen.ts`, wo `check:cli` sie messen kann. Eine Funktion, die
|
|
47
|
+
// niemand mehr ruft, wird nicht aufgehoben: Sie waere beim naechsten Mal die
|
|
48
|
+
// naheliegende, aber zu enge Wahl.
|
package/dist/argumente.js
CHANGED
|
@@ -7,22 +7,50 @@
|
|
|
7
7
|
* Titel anzulegen.
|
|
8
8
|
*/
|
|
9
9
|
export function zerlege(argv, erlaubt) {
|
|
10
|
-
const befehl = [];
|
|
11
10
|
const optionen = {};
|
|
12
11
|
for (let i = 0; i < argv.length; i++) {
|
|
13
12
|
const teil = argv[i];
|
|
13
|
+
// ⚠️ **Jedes Wort ohne `--` ist hier ein Fehler** (B-4). `zerlege` bekommt
|
|
14
|
+
// vom Verteiler nur den Teil NACH dem Befehlsnamen -- dort gehoert nichts
|
|
15
|
+
// hin ausser Optionen. Frueher landeten solche Woerter in `werte.befehl`
|
|
16
|
+
// und wurden nie wieder angesehen.
|
|
14
17
|
if (!teil.startsWith("--")) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
continue;
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
meldung: `Mit „${teil}" kann ich hier nichts anfangen. Erwartet werden nur Optionen wie „--name wert".`,
|
|
21
|
+
};
|
|
20
22
|
}
|
|
21
23
|
const gleich = teil.indexOf("=");
|
|
22
24
|
const name = gleich === -1 ? teil.slice(2) : teil.slice(2, gleich);
|
|
23
25
|
if (!erlaubt.includes(name)) {
|
|
24
26
|
return { ok: false, meldung: `Die Option „--${name}" kenne ich nicht.` };
|
|
25
27
|
}
|
|
28
|
+
// ⚠️ **Dieselbe Option zweimal ist ein FEHLER** (QA-Befund B-3, 2026-08-18).
|
|
29
|
+
//
|
|
30
|
+
// `optionen` ist ein `Record` -- die zweite Nennung ueberschrieb die erste
|
|
31
|
+
// stillschweigend. `kimal3 firma daten --firma "A" --firma "B"` lieferte
|
|
32
|
+
// die Daten von B, mit Rueckgabewert 0 und ohne jeden Hinweis. Wer die
|
|
33
|
+
// Antwort danach "fuer A" ueberschreibt, hat eine falsche Antwort auf eine
|
|
34
|
+
// richtige Frage.
|
|
35
|
+
//
|
|
36
|
+
// 🔑 Die Route ist gegen den Fall gewappnet (`getAll("firma").length > 1`,
|
|
37
|
+
// gebaut nach dem Absturz aus PR #14) -- nur bekam sie ihn ueber den
|
|
38
|
+
// CLI-Weg nie zu sehen: Hier wurde aus zwei Nennungen laengst eine.
|
|
39
|
+
// *Ein Schutz an der zweiten Tuer nuetzt nichts, wenn die erste das
|
|
40
|
+
// Problem schon weggeraeumt hat -- und man sieht ihm das beim Lesen nicht
|
|
41
|
+
// an.* Deshalb steht die Strenge HIER, wo sie alle Befehle auf einmal
|
|
42
|
+
// erreicht, und nicht in einem einzelnen Befehl.
|
|
43
|
+
//
|
|
44
|
+
// `Object.hasOwn` und nicht `name in optionen`: Letzteres findet auch
|
|
45
|
+
// geerbte Namen wie `constructor` und wuerde `--constructor` faelschlich
|
|
46
|
+
// als doppelt melden. Heute faengt `erlaubt` das ab -- aber diese Zeile
|
|
47
|
+
// soll auch dann noch stimmen, wenn jemand die Reihenfolge aendert.
|
|
48
|
+
if (Object.hasOwn(optionen, name)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
meldung: `Die Option „--${name}" steht zweimal da. Bitte nenne sie nur einmal.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
26
54
|
if (gleich !== -1) {
|
|
27
55
|
optionen[name] = teil.slice(gleich + 1);
|
|
28
56
|
continue;
|
|
@@ -35,5 +63,5 @@ export function zerlege(argv, erlaubt) {
|
|
|
35
63
|
optionen[name] = wert;
|
|
36
64
|
i++;
|
|
37
65
|
}
|
|
38
|
-
return { ok: true, werte: {
|
|
66
|
+
return { ok: true, werte: { optionen } };
|
|
39
67
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { zerlege } from "../argumente.js";
|
|
2
|
+
import { feld, ruf } from "../api.js";
|
|
3
|
+
import { schlussFehler } from "../fehler.js";
|
|
4
|
+
import { firmenZeilen } from "../firmen-liste.js";
|
|
5
|
+
import { BASIS_URL } from "../anmeldung.js";
|
|
6
|
+
const ERLAUBT = ["firma"];
|
|
7
|
+
/**
|
|
8
|
+
* Die Firmendaten holen — als JSON, unveraendert.
|
|
9
|
+
*
|
|
10
|
+
* 🔑 **Dieser Befehl INTERPRETIERT NICHTS. Er transportiert.** Die Auswertung
|
|
11
|
+
* macht Claude, gefuehrt vom Skill — das ist die KI-native Bauform und der
|
|
12
|
+
* Grund, warum es keine `--abteilung`- oder `--zeitraum`-Optionen gibt: Filtern
|
|
13
|
+
* kann Claude aus dem JSON selbst, und jede Option waere eine zweite Stelle fuer
|
|
14
|
+
* dieselbe Regel.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ Auf stdout geht das JSON und SONST NICHTS. Wer hier eine freundliche
|
|
17
|
+
* Ueberschrift dazudruckt, macht die Ausgabe als JSON unlesbar — alles fuer
|
|
18
|
+
* Menschen gehoert nach stderr (`console.error`).
|
|
19
|
+
*/
|
|
20
|
+
const HILFE = `
|
|
21
|
+
kimal3 firma daten [--firma "Name"]
|
|
22
|
+
|
|
23
|
+
Holt die Daten deiner Firma als JSON: Kennzahlen, Menschen, Projekte.
|
|
24
|
+
Die Auswertung macht Claude daraus selbst.
|
|
25
|
+
|
|
26
|
+
--firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
|
|
27
|
+
Die Absage nennt dir die Namen als fertige Befehle.
|
|
28
|
+
`;
|
|
29
|
+
export async function firmaDaten(argv) {
|
|
30
|
+
// ⚠️ VOR `zerlege`, genau wie bei `projekt neu`: Ein Schalter ist keine
|
|
31
|
+
// Option-mit-Wert, `--help` scheiterte dort sonst mit "fehlt ihr Wert".
|
|
32
|
+
// Die Terminal-Hilfe wirbt mit "Mehr zu einem Befehl: … --help" -- ein
|
|
33
|
+
// Befehl, der darauf mit einem Fehler antwortet, macht die Werbung zur Luege.
|
|
34
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
35
|
+
console.log(HILFE);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
const zerlegt = zerlege(argv, ERLAUBT);
|
|
39
|
+
if (!zerlegt.ok) {
|
|
40
|
+
console.error(zerlegt.meldung);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
// ⚠️ `!== undefined` und NICHT auf Wahrheitswert pruefen: `zerlege`
|
|
44
|
+
// unterscheidet ausdruecklich "nicht angegeben" (`undefined`) von
|
|
45
|
+
// "ausdruecklich leer" (`--firma=` -> `""`), und `check:cli` sichert genau
|
|
46
|
+
// das ab. Wer hier auf Wahrheit prueft, wirft beides zusammen: `--firma=`
|
|
47
|
+
// lieferte dann die Daten der einzigen Firma, als haette niemand gefragt.
|
|
48
|
+
const wunsch = zerlegt.werte.optionen.firma;
|
|
49
|
+
const pfad = wunsch !== undefined
|
|
50
|
+
? `/api/cli/firma?firma=${encodeURIComponent(wunsch)}`
|
|
51
|
+
: "/api/cli/firma";
|
|
52
|
+
const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true });
|
|
53
|
+
if (antwort.status === 200) {
|
|
54
|
+
// 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg (Review-Befund Runde 1):
|
|
55
|
+
// `ruf` verschluckt Parse-Fehler zu `null`. Ein Zwischending im Netz --
|
|
56
|
+
// etwa ein WLAN-Anmeldeportal -- antwortet mit `200` und HTML. Ohne diese
|
|
57
|
+
// Wache druckte der Befehl das Wort `null` auf stdout und meldete 0.
|
|
58
|
+
// Und die 0 ist die Schnittstelle: Claude liest sie als "erledigt" und
|
|
59
|
+
// haelt `null` fuer die Daten der Firma.
|
|
60
|
+
if (antwort.koerper === null || typeof antwort.koerper !== "object") {
|
|
61
|
+
console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
|
|
62
|
+
return 3;
|
|
63
|
+
}
|
|
64
|
+
console.log(JSON.stringify(antwort.koerper, null, 2));
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
|
|
68
|
+
// 402 VOR 401: Beide heissen "kommst hier nicht rein", verlangen aber
|
|
69
|
+
// Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` --
|
|
70
|
+
// und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
|
|
71
|
+
if (antwort.status === 402) {
|
|
72
|
+
console.error(meldung);
|
|
73
|
+
return 4;
|
|
74
|
+
}
|
|
75
|
+
if (antwort.status === 401) {
|
|
76
|
+
console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
if (antwort.status === 409) {
|
|
80
|
+
const grund = feld(antwort.koerper, "grund");
|
|
81
|
+
const koerper = antwort.koerper;
|
|
82
|
+
const namen = Array.isArray(koerper?.firmen)
|
|
83
|
+
? koerper.firmen.filter((x) => typeof x === "string")
|
|
84
|
+
: [];
|
|
85
|
+
console.error(meldung);
|
|
86
|
+
// Die Namen als FERTIGE Befehle ausgeben, nicht als blosse Liste: Der
|
|
87
|
+
// naechste Schritt ist dann Kopieren statt Abtippen -- und ein Firmenname
|
|
88
|
+
// mit Leerzeichen braucht Anfuehrungszeichen, an die sonst niemand denkt.
|
|
89
|
+
//
|
|
90
|
+
// ⚠️ Welche Namen einen Befehl bekommen, entscheidet `firmenZeilen` (B-5):
|
|
91
|
+
// Ein doppelt vergebener Name bekommt KEINEN -- er waere ein Vorschlag,
|
|
92
|
+
// der garantiert wieder in derselben Absage endet.
|
|
93
|
+
const zeilen = firmenZeilen(namen);
|
|
94
|
+
if (zeilen.length > 0) {
|
|
95
|
+
console.error("\n Deine Firmen:");
|
|
96
|
+
zeilen.forEach((z) => console.error(z));
|
|
97
|
+
}
|
|
98
|
+
if (grund === "keine-firma") {
|
|
99
|
+
console.error(`\n Eine Firma anlegen: ${BASIS_URL}/firma/neu`);
|
|
100
|
+
}
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
// ⚠️ Die 503 landet HIER und nicht oben bei der 402: Rueckgabewert 3 heisst
|
|
104
|
+
// "nochmal versuchen kann helfen", 4 heisst laut SKILL.md ausdruecklich
|
|
105
|
+
// "nicht wiederholen". Ein Aussetzer gehoert in den ersten Topf.
|
|
106
|
+
//
|
|
107
|
+
// 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
|
|
108
|
+
// insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
|
|
109
|
+
// "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
|
|
110
|
+
const schluss = schlussFehler(antwort.status, meldung);
|
|
111
|
+
console.error(schluss.text);
|
|
112
|
+
return schluss.wert;
|
|
113
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { feld, ruf
|
|
1
|
+
import { feld, ruf } from "../api.js";
|
|
2
|
+
import { schlussFehler } from "../fehler.js";
|
|
3
|
+
import { MAX_PROJEKTE_ANZEIGE, antwortHatProjekteFeld, titelFeldLesen, projekteAusAntwort, projektZeilen, titelAusAlterAntwort, } from "../projekt-zeilen.js";
|
|
2
4
|
import { BASIS_URL } from "../anmeldung.js";
|
|
3
5
|
/**
|
|
4
6
|
* Die eigenen Projekte anzeigen.
|
|
@@ -6,24 +8,98 @@ import { BASIS_URL } from "../anmeldung.js";
|
|
|
6
8
|
* 🔑 Wofuer das da ist: Damit Claude VOR dem Eintragen nachsehen kann. Ohne
|
|
7
9
|
* diesen Befehl steht dasselbe Projekt irgendwann zweimal auf einer
|
|
8
10
|
* oeffentlichen Seite mit dem Namen eines Menschen darauf.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ Seit dem 2026-08-18 (Befund L-4) stehen auch Live-Datum und Zahlen da.
|
|
13
|
+
* Der Slash-Befehl `/k3liste` versprach das schon vorher woertlich -- und die
|
|
14
|
+
* Ausgabe gab nur Titel her. *Eine Anleitung, die mehr verspricht als das
|
|
15
|
+
* Werkzeug hergibt, laesst Claude entweder raten oder sich entschuldigen.*
|
|
9
16
|
*/
|
|
10
17
|
export async function projektListe() {
|
|
11
18
|
const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true });
|
|
12
19
|
if (antwort.status === 200) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
// 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dieselbe Wache wie
|
|
21
|
+
// in `firma-daten.ts`: `ruf` verschluckt Parse-Fehler zu `null`, und ein
|
|
22
|
+
// WLAN-Anmeldeportal antwortet mit `200` und HTML.
|
|
23
|
+
const projekte = projekteAusAntwort(antwort.koerper);
|
|
24
|
+
if (!projekte) {
|
|
25
|
+
/*
|
|
26
|
+
🔴 **B-7 in der GEGENRICHTUNG** (Befund L-7, 2026-08-18, im Live-Durchlauf
|
|
27
|
+
selbst hineingelaufen). Bisher wurde jede unlesbare Antwort gleich
|
|
28
|
+
behandelt: „Die Antwort war nicht lesbar. Bitte gleich noch einmal."
|
|
29
|
+
|
|
30
|
+
Es gibt aber einen Fall, in dem Wiederholen NIE hilft: Die CLI ist neuer
|
|
31
|
+
als die App. Dann antwortet der Server ordentlich — nur eben in der
|
|
32
|
+
alten Form, mit `titel` statt `projekte`. Das passiert bei jedem
|
|
33
|
+
Deployment im Fenster zwischen npm und Vercel, und es traf hier den
|
|
34
|
+
eigenen Entwickler.
|
|
35
|
+
|
|
36
|
+
*Ein Rat, der garantiert nicht hilft, ist schlimmer als kein Rat: Er
|
|
37
|
+
sieht aus wie ein Ausweg.* -- dieselbe Lehre wie bei B-5.
|
|
38
|
+
|
|
39
|
+
Die alte Form ist erkennbar, also wird sie auch bedient: Der Mensch
|
|
40
|
+
bekommt seine Liste, nur ohne die neuen Angaben. Nichts kaputt, nichts
|
|
41
|
+
zu wiederholen.
|
|
42
|
+
*/
|
|
43
|
+
const alt = titelAusAlterAntwort(antwort.koerper);
|
|
44
|
+
if (alt) {
|
|
45
|
+
if (alt.length === 0) {
|
|
46
|
+
console.log("Du hast noch keine Projekte eingetragen.");
|
|
47
|
+
console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
console.log(`\n Deine Projekte (${alt.length} von ${MAX_PROJEKTE_ANZEIGE}):`);
|
|
51
|
+
alt.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
52
|
+
console.log("\n (Diese Version der App liefert noch keine Zahlen zu den");
|
|
53
|
+
console.log(" Projekten. Du hast nichts falsch gemacht — sie kommen mit");
|
|
54
|
+
console.log(" dem nächsten Update der App von selbst dazu.)");
|
|
55
|
+
console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
/*
|
|
59
|
+
🔴 **Die dritte Richtung: die App ist NEUER als dieses Werkzeug**
|
|
60
|
+
(Review-Runde 4, 2026-08-18). Steht `projekte` in der Antwort und war
|
|
61
|
+
sie trotzdem nicht lesbar, hat ein Eintrag eine Form, die diese
|
|
62
|
+
Fassung nicht kennt. Das ist **stabil** -- Wiederholen aendert daran
|
|
63
|
+
nichts, bis aktualisiert wird. Vorher fiel es in denselben Topf wie
|
|
64
|
+
eine kaputte Antwort und riet "gleich noch einmal".
|
|
65
|
+
*/
|
|
66
|
+
if (antwortHatProjekteFeld(antwort.koerper)) {
|
|
67
|
+
/*
|
|
68
|
+
🔑 **Erst geben, was da ist** (Review-Runde 6, 2026-08-18). Die neue
|
|
69
|
+
Route liefert `titel` und `projekte` IMMER gemeinsam. Aendert eine
|
|
70
|
+
spaetere App-Fassung die Form eines Eintrags, sind die Titel trotzdem
|
|
71
|
+
lesbar -- sie dann NICHT zu zeigen und stattdessen nur "aktualisiere"
|
|
72
|
+
zu sagen, wirft Daten weg, die man in der Hand haelt.
|
|
73
|
+
*Wer dem Menschen etwas geben kann, gibt es ihm -- und sagt dazu,
|
|
74
|
+
was fehlt.*
|
|
75
|
+
*/
|
|
76
|
+
const titel = titelFeldLesen(antwort.koerper);
|
|
77
|
+
if (titel && titel.length > 0) {
|
|
78
|
+
console.log(`\n Deine Projekte (${titel.length} von ${MAX_PROJEKTE_ANZEIGE}):`);
|
|
79
|
+
titel.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
80
|
+
console.log("\n (Die Zahlen dazu kann diese Fassung von kimal3 nicht lesen —");
|
|
81
|
+
console.log(" die App ist neuer als das Werkzeug. Du hast nichts falsch");
|
|
82
|
+
console.log(" gemacht; `npm install -g kimal3` holt sie.)");
|
|
83
|
+
console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
console.error("Diese Fassung von kimal3 versteht die Antwort der App nicht.");
|
|
87
|
+
console.error("Du hast nichts falsch gemacht — das Werkzeug ist älter als die App.");
|
|
88
|
+
console.error(" npm install -g kimal3");
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
|
|
16
92
|
return 3;
|
|
17
93
|
}
|
|
18
94
|
// Eine leere Liste ist KEIN Fehler — sie ist eine gueltige Antwort auf
|
|
19
95
|
// eine gueltige Frage. Deshalb 0 und ein Satz, kein Fehlertext.
|
|
20
|
-
if (
|
|
96
|
+
if (projekte.length === 0) {
|
|
21
97
|
console.log("Du hast noch keine Projekte eingetragen.");
|
|
22
98
|
console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
|
|
23
99
|
return 0;
|
|
24
100
|
}
|
|
25
|
-
console.log(`\n Deine Projekte (${
|
|
26
|
-
|
|
101
|
+
console.log(`\n Deine Projekte (${projekte.length} von ${MAX_PROJEKTE_ANZEIGE}):\n`);
|
|
102
|
+
projektZeilen(projekte).forEach((z) => console.log(z));
|
|
27
103
|
console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
|
|
28
104
|
return 0;
|
|
29
105
|
}
|
|
@@ -48,6 +124,11 @@ export async function projektListe() {
|
|
|
48
124
|
// versuchen kann helfen"; 4 heisst laut SKILL.md ausdruecklich "nicht
|
|
49
125
|
// wiederholen". Ein Lesefehler an der Pro-Schranke gehoert in den ersten
|
|
50
126
|
// Topf. (Review-Befund 2026-08-14.)
|
|
51
|
-
|
|
52
|
-
|
|
127
|
+
//
|
|
128
|
+
// 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
|
|
129
|
+
// insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
|
|
130
|
+
// "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
|
|
131
|
+
const schluss = schlussFehler(antwort.status, meldung);
|
|
132
|
+
console.error(schluss.text);
|
|
133
|
+
return schluss.wert;
|
|
53
134
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { feld, ruf } from "../api.js";
|
|
2
|
+
import { schlussFehler } from "../fehler.js";
|
|
2
3
|
import { BASIS_URL } from "../anmeldung.js";
|
|
3
4
|
import { zerlege } from "../argumente.js";
|
|
4
5
|
const OPTIONEN = [
|
|
@@ -77,6 +78,11 @@ export async function projektNeu(argv) {
|
|
|
77
78
|
// versuchen kann helfen"; 4 heisst laut SKILL.md ausdruecklich "nicht
|
|
78
79
|
// wiederholen". Ein Lesefehler an der Pro-Schranke gehoert in den ersten
|
|
79
80
|
// Topf. (Review-Befund 2026-08-14.)
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
//
|
|
82
|
+
// 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
|
|
83
|
+
// insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
|
|
84
|
+
// "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
|
|
85
|
+
const schluss = schlussFehler(antwort.status, meldung);
|
|
86
|
+
console.error(schluss.text);
|
|
87
|
+
return schluss.wert;
|
|
82
88
|
}
|
|
@@ -1,22 +1,61 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Namen, die frueher einmal ausgeliefert wurden und heute nicht mehr.
|
|
7
|
+
*
|
|
8
|
+
* 🔑 **Wofuer das da ist** (Review-Befund, 2026-08-18): Die Befehle landen im
|
|
9
|
+
* GEMEINSAMEN Ordner `~/.claude/commands/` und werden dort nur ueberschrieben,
|
|
10
|
+
* nie entfernt. Wird ein Befehl spaeter umbenannt oder gestrichen, ueberlebt
|
|
11
|
+
* die alte Datei jedes `skill install` und bietet weiter Anweisungen an, die
|
|
12
|
+
* auf einen nicht mehr vorhandenen Unterbefehl zeigen. `SKILL.md` kann das
|
|
13
|
+
* nicht passieren -- es ist EINE Datei, die ueberschrieben wird.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ Warum eine Liste und nicht "alles loeschen, was wir nicht ausliefern":
|
|
16
|
+
* Der Ordner gehoert dem Menschen. Ein `k3*.md`, das er selbst angelegt hat,
|
|
17
|
+
* duerfen wir nicht wegraeumen. **Wer einen Befehl umbenennt oder entfernt,
|
|
18
|
+
* traegt den alten Namen hier ein** -- sonst bleibt er beim Nutzer stehen.
|
|
19
|
+
*/
|
|
20
|
+
const VERALTETE_BEFEHLE = [];
|
|
21
|
+
/**
|
|
22
|
+
* Woran wir unsere eigenen Befehlsdateien erkennen.
|
|
23
|
+
*
|
|
24
|
+
* Steht die Marke nicht drin, hat ein Mensch die Datei geschrieben -- dann
|
|
25
|
+
* wird sie NICHT ueberschrieben, sondern uebersprungen und gemeldet.
|
|
26
|
+
*/
|
|
27
|
+
const MARKE = "kimal3:erzeugt";
|
|
28
|
+
/**
|
|
29
|
+
* Legt das Skill UND die Slash-Befehle GLOBAL ab, damit Claude dieses Werkzeug
|
|
30
|
+
* ueberall kennt -- und damit der Mensch es auch selbst ausloesen kann.
|
|
31
|
+
*
|
|
32
|
+
* 🔑 **Warum zwei Ziele und nicht eins** (Entscheidung Dennis, 2026-08-18):
|
|
33
|
+
* Das Skill ist der Ausloeser fuer CLAUDE ("das Projekt ist fertig" -> Claude
|
|
34
|
+
* erkennt die Absicht). Die Slash-Befehle sind der Ausloeser fuer den
|
|
35
|
+
* MENSCHEN ("/k3firma …" -> er muss sich keinen Satz merken). Das eine ist
|
|
36
|
+
* wahrscheinlich, das andere sicher. Wer nur das Skill installiert, laesst den
|
|
37
|
+
* sicheren Weg weg.
|
|
7
38
|
*
|
|
8
39
|
* Der Pfad zur eigenen Datei kommt ueber `import.meta.url` und NICHT ueber
|
|
9
40
|
* `process.cwd()` — die Quelle liegt im installierten Paket, das Ziel im
|
|
10
41
|
* Ordner des Menschen. Wer beides verwechselt, kopiert eine Datei auf sich
|
|
11
42
|
* selbst.
|
|
12
43
|
*
|
|
13
|
-
* `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill
|
|
44
|
+
* `dist/befehle/skill-install.js` -> zwei Ebenen hoch -> `skill/…`.
|
|
14
45
|
*/
|
|
15
46
|
export async function skillInstall() {
|
|
16
47
|
const quelle = fileURLToPath(new URL("../../skill/SKILL.md", import.meta.url));
|
|
17
48
|
if (!existsSync(quelle)) {
|
|
18
|
-
|
|
19
|
-
|
|
49
|
+
/*
|
|
50
|
+
🔴 **`1`, nicht `3`** (Review-Befund, 2026-08-18). Ein unvollstaendiges
|
|
51
|
+
Paket wird durch Wiederholen nie vollstaendig. `3` heisst laut SKILL.md
|
|
52
|
+
ausdruecklich "einmal wiederholen" -- Claude liest die ZAHL, nicht den
|
|
53
|
+
Text, und wuerde einen Rat geben, der garantiert nicht hilft. Genau der
|
|
54
|
+
Fehlertyp, den dieser Zweig bei B-5, B-7 und L-7 behoben hat.
|
|
55
|
+
*/
|
|
56
|
+
console.error("Das Skill liegt nicht im Paket. Bitte kimal3 neu installieren:");
|
|
57
|
+
console.error(" npm install -g kimal3");
|
|
58
|
+
return 1;
|
|
20
59
|
}
|
|
21
60
|
// 🔑 GLOBAL, nicht projektlokal (Fund von Dennis, 2026-08-13).
|
|
22
61
|
// Gemessen: In ~/.claude/skills liegen 34 Skills, im Projekt selbst kein
|
|
@@ -39,6 +78,232 @@ export async function skillInstall() {
|
|
|
39
78
|
}
|
|
40
79
|
console.log(gabEsSchon ? "✓ Skill aktualisiert." : "✓ Skill angelegt.");
|
|
41
80
|
console.log(` ${ziel}`);
|
|
42
|
-
|
|
81
|
+
const namen = slashBefehleInstallieren();
|
|
82
|
+
if (namen === "kaputt") {
|
|
83
|
+
/*
|
|
84
|
+
⚠️ **Was hier zaehlt, ist die TEILWIRKUNG** (Review-Runde 2, 2026-08-18):
|
|
85
|
+
Das Skill steht oben bereits als angelegt gemeldet. Ohne diesen Satz
|
|
86
|
+
liest der Mensch ein "✓" und danach einen Fehler und weiss nicht, was
|
|
87
|
+
nun gilt. *Ein halber Erfolg ist weder ein Erfolg noch ein Totalausfall
|
|
88
|
+
-- er ist ein eigener Zustand und braucht einen eigenen Satz.*
|
|
89
|
+
*/
|
|
90
|
+
console.error(" Das Skill ist installiert, die Slash-Befehle sind es NICHT.");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
const { geschrieben, uebersprungen, unlesbar, gestoert } = namen;
|
|
94
|
+
if (geschrieben.length > 0) {
|
|
95
|
+
console.log(`✓ ${geschrieben.length} Slash-Befehle: ${geschrieben.map((n) => `/${n}`).join(" · ")}`);
|
|
96
|
+
console.log(` ${join(homedir(), ".claude", "commands")}`);
|
|
97
|
+
}
|
|
98
|
+
// ⚠️ Uebersprungene Dateien gehoeren in die AUSGABE, nicht nur in die Logik.
|
|
99
|
+
// Ein Mensch, dem stillschweigend ein Befehl fehlt, sucht den Fehler bei
|
|
100
|
+
// sich. (Review-Runde 2, 2026-08-18.)
|
|
101
|
+
if (uebersprungen.length > 0) {
|
|
102
|
+
console.log(`⚠ ${uebersprungen.length} übersprungen: ${uebersprungen.map((n) => `/${n}`).join(" · ")}`);
|
|
103
|
+
console.log(" Dort liegt bereits eine Datei, die nicht von kimal3 stammt —");
|
|
104
|
+
console.log(" sie bleibt unangetastet. Wer die kimal3-Fassung will, benennt");
|
|
105
|
+
console.log(" die eigene um und führt den Befehl erneut aus.");
|
|
106
|
+
}
|
|
107
|
+
// 🔴 Ein LESEFEHLER bekommt einen eigenen Satz (Review-Runde 6): Er sieht
|
|
108
|
+
// aus wie Fremdbesitz, ist aber keiner -- und der Rat "benenn deine Datei
|
|
109
|
+
// um" waere hier falsch.
|
|
110
|
+
if (unlesbar.length > 0) {
|
|
111
|
+
console.log(`⚠ ${unlesbar.length} nicht lesbar: ${unlesbar.map((n) => `/${n}`).join(" · ")}`);
|
|
112
|
+
console.log(" Diese Dateien liessen sich nicht öffnen — vielleicht sind sie");
|
|
113
|
+
console.log(" gerade in Benutzung. Sie wurden nicht angefasst; ein zweiter");
|
|
114
|
+
console.log(" Versuch gleich kann klappen.");
|
|
115
|
+
}
|
|
116
|
+
/*
|
|
117
|
+
🔴 **Kein Slash-Befehl installiert ist kein Erfolg** (Review-Runde 5,
|
|
118
|
+
2026-08-18). Liegen im Ordner lauter fremde `k3*.md`, landet ALLES in
|
|
119
|
+
`uebersprungen`, `geschrieben` ist leer -- und der Lauf endete trotzdem
|
|
120
|
+
auf `0`. Laut der eigenen Tabelle in `SKILL.md` heisst `0` "erledigt".
|
|
121
|
+
|
|
122
|
+
*Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall.*
|
|
123
|
+
|
|
124
|
+
⚠️ Die Zahl haengt am GRUND (Review-Runde 6): War der Ordner nur belegt,
|
|
125
|
+
ist der Zustand stabil -> `1`. Ging das Schreiben schief oder war eine
|
|
126
|
+
Datei nicht lesbar, kann ein zweiter Versuch helfen -> `3`.
|
|
127
|
+
*/
|
|
128
|
+
if (geschrieben.length === 0) {
|
|
129
|
+
const wiederholbar = gestoert || unlesbar.length > 0;
|
|
130
|
+
console.error(" Es wurde KEIN Slash-Befehl installiert.");
|
|
131
|
+
console.error(" Das Skill selbst ist installiert und funktioniert.");
|
|
132
|
+
return wiederholbar ? 3 : 1;
|
|
133
|
+
}
|
|
134
|
+
/*
|
|
135
|
+
⚠️ **Eine unvollstaendige Installation als solche melden** (Review-Runde 6).
|
|
136
|
+
Scheitert `copyFileSync` bei Datei 3 von 4, sind zwei Befehle da -- der
|
|
137
|
+
Lauf ist weder gelungen noch gescheitert. Frueher warf der `catch` die
|
|
138
|
+
Teilwirkung weg und meldete alles als misslungen.
|
|
139
|
+
*/
|
|
140
|
+
if (gestoert) {
|
|
141
|
+
console.error(" Nicht alle Slash-Befehle konnten geschrieben werden.");
|
|
142
|
+
console.error(" Die oben genannten stehen; ein zweiter Lauf holt den Rest.");
|
|
143
|
+
return 3;
|
|
144
|
+
}
|
|
145
|
+
/*
|
|
146
|
+
⚠️ **Nicht auf einen Befehl verweisen, den wir gerade NICHT installiert
|
|
147
|
+
haben** (Review-Runde 3, 2026-08-18, gemessen). Mit einer fremden
|
|
148
|
+
`k3.md` im Ordner stand hier woertlich "Tipp /k3 in Claude Code" --
|
|
149
|
+
ausgerechnet der eine Befehl, den der Lauf soeben abgelehnt hatte.
|
|
150
|
+
*Ein Hinweis auf etwas, das es nicht gibt, sieht aus wie ein Fehler des
|
|
151
|
+
Menschen.*
|
|
152
|
+
*/
|
|
153
|
+
if (geschrieben.includes("k3")) {
|
|
154
|
+
console.log(" Tipp /k3 in Claude Code. (Taucht nichts auf, einmal neu starten.)");
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.log(` Tipp /${geschrieben[0]} in Claude Code. (Taucht nichts auf, einmal neu starten.)`);
|
|
158
|
+
}
|
|
43
159
|
return 0;
|
|
44
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Kopiert die Slash-Befehle nach `~/.claude/commands/`.
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ **FLACH, nicht in einen Unterordner** — und das ist keine Bequemlichkeit:
|
|
165
|
+
* Ein Unterordner `kimal3/` macht aus `/k3firma` ein `/kimal3:k3firma`. Dann
|
|
166
|
+
* findet man die Familie nicht mehr, indem man `/k3` tippt, und genau das war
|
|
167
|
+
* die Anforderung (Dennis, 2026-08-18): einmal `/k3` eingeben und alles sehen.
|
|
168
|
+
* Der gemeinsame Namensanfang ERSETZT hier den Ordner.
|
|
169
|
+
*
|
|
170
|
+
* Rueckgabe: die Namen ohne `.md` — oder `"kaputt"` (Paket unvollstaendig,
|
|
171
|
+
* Wiederholen hilft NIE) bzw. `"stoerung"` (Schreiben ging schief, Wiederholen
|
|
172
|
+
* kann helfen). *Zwei Fehlerfaelle, die Gegenteiliges verlangen, duerfen nicht
|
|
173
|
+
* denselben Namen tragen.*
|
|
174
|
+
*/
|
|
175
|
+
function slashBefehleInstallieren() {
|
|
176
|
+
const quellOrdner = fileURLToPath(new URL("../../skill/commands", import.meta.url));
|
|
177
|
+
if (!existsSync(quellOrdner)) {
|
|
178
|
+
console.error("Die Slash-Befehle liegen nicht im Paket. Bitte kimal3 neu installieren.");
|
|
179
|
+
return "kaputt";
|
|
180
|
+
}
|
|
181
|
+
const dateien = readdirSync(quellOrdner).filter((n) => n.endsWith(".md")).sort();
|
|
182
|
+
if (dateien.length === 0) {
|
|
183
|
+
console.error("Im Paket liegt kein einziger Slash-Befehl. Bitte kimal3 neu installieren.");
|
|
184
|
+
return "kaputt";
|
|
185
|
+
}
|
|
186
|
+
const zielOrdner = join(homedir(), ".claude", "commands");
|
|
187
|
+
const geschrieben = [];
|
|
188
|
+
const uebersprungen = [];
|
|
189
|
+
const unlesbar = [];
|
|
190
|
+
let gestoert = false;
|
|
191
|
+
try {
|
|
192
|
+
mkdirSync(zielOrdner, { recursive: true });
|
|
193
|
+
for (const datei of dateien) {
|
|
194
|
+
const ziel = join(zielOrdner, datei);
|
|
195
|
+
/*
|
|
196
|
+
🔴 **Fremde Dateien NICHT ueberschreiben** (Review-Runde 2, 2026-08-18).
|
|
197
|
+
Der Ordner gehoert dem Menschen. Der Kommentar bei VERALTETE_BEFEHLE
|
|
198
|
+
argumentiert, dass wir eine selbstgeschriebene `k3*.md` nicht loeschen
|
|
199
|
+
duerfen -- *Ueberschreiben zerstoert sie genauso gruendlich*, nur
|
|
200
|
+
unauffaelliger.
|
|
201
|
+
|
|
202
|
+
Woran wir unsere eigenen erkennen: an der Marke `kimal3:erzeugt`. Ein
|
|
203
|
+
blosser Inhaltsvergleich taugt nicht -- eine AELTERE Fassung von uns
|
|
204
|
+
unterscheidet sich auch, und die soll ja gerade ersetzt werden.
|
|
205
|
+
*/
|
|
206
|
+
if (existsSync(ziel)) {
|
|
207
|
+
const vorhanden = leseWennMoeglich(ziel);
|
|
208
|
+
/*
|
|
209
|
+
⚠️ **Ein LESEFEHLER ist kein Fremdbesitz** (Review-Runde 6,
|
|
210
|
+
2026-08-18). Vorher wurde ein gescheitertes `readFileSync` zu einem
|
|
211
|
+
leeren Text -- dann fehlt die Marke zwangslaeufig, und der Mensch las
|
|
212
|
+
"dort liegt eine Datei, die nicht von kimal3 stammt" fuer eine Datei,
|
|
213
|
+
die kimal3 selbst geschrieben hat. *Ein Fehlerfall unter dem Namen
|
|
214
|
+
eines anderen, zum vierten Mal in diesem Zweig.*
|
|
215
|
+
*/
|
|
216
|
+
if (vorhanden === null) {
|
|
217
|
+
unlesbar.push(datei);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!vorhanden.includes(MARKE)) {
|
|
221
|
+
uebersprungen.push(datei);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
copyFileSync(join(quellOrdner, datei), ziel);
|
|
226
|
+
geschrieben.push(datei);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (fehler) {
|
|
230
|
+
/*
|
|
231
|
+
⚠️ **Die Teilwirkung ueberlebt den Fehler** (Review-Runde 6, 2026-08-18).
|
|
232
|
+
Vorher warf dieser `catch` alles weg und meldete "stoerung" -- auch wenn
|
|
233
|
+
zwei von vier Befehlen bereits geschrieben waren. Der Mensch hoerte "die
|
|
234
|
+
Slash-Befehle sind NICHT installiert", waehrend die Haelfte dastand.
|
|
235
|
+
`geschrieben` und `uebersprungen` sind ausserhalb des `try` deklariert,
|
|
236
|
+
genau dafuer.
|
|
237
|
+
*/
|
|
238
|
+
console.error(`Die Slash-Befehle liessen sich nicht alle schreiben: ${String(fehler)}`);
|
|
239
|
+
gestoert = true;
|
|
240
|
+
}
|
|
241
|
+
raeumeVeralteteAuf(zielOrdner, VERALTETE_BEFEHLE);
|
|
242
|
+
return {
|
|
243
|
+
geschrieben: geschrieben.map((n) => n.replace(/\.md$/, "")),
|
|
244
|
+
uebersprungen: uebersprungen.map((n) => n.replace(/\.md$/, "")),
|
|
245
|
+
unlesbar: unlesbar.map((n) => n.replace(/\.md$/, "")),
|
|
246
|
+
gestoert,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Liest eine Datei — `null`, wenn sie sich nicht lesen laesst. */
|
|
250
|
+
function leseWennMoeglich(pfad) {
|
|
251
|
+
try {
|
|
252
|
+
return readFileSync(pfad, "utf8");
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Entfernt Befehlsdateien, die frueher einmal ausgeliefert wurden.
|
|
260
|
+
*
|
|
261
|
+
* 🔴 **Warum das eine eigene, exportierte Funktion ist** (Review-Runde 3,
|
|
262
|
+
* 2026-08-18): `VERALTETE_BEFEHLE` ist leer, also ist diese Schleife noch nie
|
|
263
|
+
* gelaufen -- und `check:cli` "belegte" sie nur ueber eine Textsuche. Genau
|
|
264
|
+
* die Falle aus der Projekt-Notiz *wache-am-echten-fehlerfall-pruefen*.
|
|
265
|
+
* Mit dem Zielordner als Parameter laesst sie sich gegen einen Wegwerf-Ordner
|
|
266
|
+
* mit einer echten Datei fahren.
|
|
267
|
+
*
|
|
268
|
+
* 🔴 **Und sie loescht NUR eigene Dateien** (Review-Runde 6, 2026-08-18).
|
|
269
|
+
* Vorher entfernte sie blind, was auf der Liste stand -- waehrend die
|
|
270
|
+
* Kopierschleife siebzig Zeilen hoeher genau solche Dateien verschont und dem
|
|
271
|
+
* Menschen sagt "sie bleibt unangetastet". *Zwei Haelften desselben Befehls
|
|
272
|
+
* mit gegensaetzlicher Haltung zum selben Ordner.* Wird `/k3liste` spaeter
|
|
273
|
+
* umbenannt, verloere ein Mensch mit eigener `k3liste.md` sie ohne Rueckfrage.
|
|
274
|
+
*
|
|
275
|
+
* Gibt zurueck, was wirklich entfernt wurde -- eine Behauptung waere hier
|
|
276
|
+
* genau das Falsche.
|
|
277
|
+
*/
|
|
278
|
+
export function raeumeVeralteteAuf(zielOrdner, veraltet) {
|
|
279
|
+
const entfernt = [];
|
|
280
|
+
for (const eintrag of veraltet) {
|
|
281
|
+
/*
|
|
282
|
+
🔴 **Beide Schreibweisen annehmen** (Review-Runde 5, 2026-08-18). Der
|
|
283
|
+
Doc-Block sagt "wer einen Befehl umbenennt, traegt den alten NAMEN hier
|
|
284
|
+
ein" -- gejoint wird aber ein DATEIname. Wer `"k3alt"` eintraegt, bekam
|
|
285
|
+
keinen Fehler: nichts wird entfernt, nichts gemeldet.
|
|
286
|
+
*/
|
|
287
|
+
const alt = eintrag.endsWith(".md") ? eintrag : `${eintrag}.md`;
|
|
288
|
+
const weg = join(zielOrdner, alt);
|
|
289
|
+
try {
|
|
290
|
+
if (!existsSync(weg))
|
|
291
|
+
continue;
|
|
292
|
+
const inhalt = leseWennMoeglich(weg);
|
|
293
|
+
if (inhalt === null || !inhalt.includes(MARKE)) {
|
|
294
|
+
// Nicht von uns (oder nicht lesbar) -> nicht anfassen. Still, weil es
|
|
295
|
+
// der Normalfall ist, dass dort gar nichts liegt.
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
rmSync(weg);
|
|
299
|
+
entfernt.push(alt);
|
|
300
|
+
console.log(` (veralteten Befehl /${alt.replace(/\.md$/, "")} entfernt)`);
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Bewusst still: Die Befehle sind installiert. Eine liegengebliebene
|
|
304
|
+
// alte Datei ist ein Schoenheitsfehler, kein Grund, den Erfolg zu
|
|
305
|
+
// widerrufen. (Review-Runde 2.)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return entfernt;
|
|
309
|
+
}
|