kimal3 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +135 -0
  2. package/dist/api.js +16 -7
  3. package/dist/arbeitsplatz-dateien.js +62 -30
  4. package/dist/argumente.js +25 -97
  5. package/dist/befehle/arbeitsplatz.js +51 -68
  6. package/dist/befehle/firma-daten.js +24 -26
  7. package/dist/befehle/kontext-fragen.js +62 -44
  8. package/dist/befehle/kontext-gemeinsam.js +86 -43
  9. package/dist/befehle/kontext-hoch.js +40 -46
  10. package/dist/befehle/kontext-runter.js +70 -51
  11. package/dist/befehle/login.js +23 -9
  12. package/dist/befehle/logout.js +9 -4
  13. package/dist/befehle/projekt-liste.js +24 -29
  14. package/dist/befehle/projekt-neu.js +25 -31
  15. package/dist/befehle/skill-install.js +78 -49
  16. package/dist/befehle/skill-tausch.js +28 -0
  17. package/dist/befehle/wache.js +12 -6
  18. package/dist/befehle/werkbank.js +118 -88
  19. package/dist/befehle-katalog-en.js +185 -0
  20. package/dist/befehle-katalog.js +10 -49
  21. package/dist/befehlswoerter.js +69 -0
  22. package/dist/einstellungen-mischen.js +2 -2
  23. package/dist/einstellungen-schreiben.js +8 -5
  24. package/dist/fehler.js +5 -5
  25. package/dist/firmen-liste.js +42 -5
  26. package/dist/format.js +41 -0
  27. package/dist/fragen-lesen.js +3 -3
  28. package/dist/hilfe.js +24 -0
  29. package/dist/kimal3.js +77 -71
  30. package/dist/kontext-dateien.js +30 -23
  31. package/dist/kontext-hoch-zeile.js +5 -3
  32. package/dist/kontext-rat.js +151 -0
  33. package/dist/kontext-stand.js +68 -210
  34. package/dist/nachricht.js +39 -15
  35. package/dist/projekt-neu-firma.js +9 -9
  36. package/dist/projekt-zeilen.js +40 -54
  37. package/dist/slash-befehle.js +36 -0
  38. package/dist/sprache.js +114 -0
  39. package/dist/texte/allgemein.js +99 -0
  40. package/dist/texte/arbeitsplatz.js +135 -0
  41. package/dist/texte/kontext-hoch.js +128 -0
  42. package/dist/texte/kontext.js +171 -0
  43. package/dist/texte/projekte.js +126 -0
  44. package/dist/texte/skill.js +87 -0
  45. package/dist/texte/wache.js +36 -0
  46. package/dist/texte/werkbank.js +161 -0
  47. package/dist/wache-rechnung.js +27 -40
  48. package/dist/wache-schalter.js +40 -5
  49. package/dist/werkbank-name.js +28 -10
  50. package/dist/wort.js +98 -0
  51. package/package.json +27 -27
  52. package/skill/SKILL.md +18 -10
  53. package/skill/commands/k3.md +1 -1
  54. package/skill/commands/k3feierabend.md +99 -97
  55. package/skill/commands/k3kontext.md +2 -2
  56. package/skill/commands/k3kurs.md +6 -5
  57. package/skill/commands/k3start.md +2 -1
  58. package/skill/en/SKILL.md +451 -0
  59. package/skill/en/commands/k3.md +106 -0
  60. package/skill/en/commands/k3analyse.md +59 -0
  61. package/skill/en/commands/k3context.md +92 -0
  62. package/skill/en/commands/k3course.md +54 -0
  63. package/skill/en/commands/k3list.md +26 -0
  64. package/skill/en/commands/k3project.md +50 -0
  65. package/skill/en/commands/k3start.md +47 -0
  66. package/skill/en/commands/k3wrapup.md +99 -0
  67. package/dist/zz-kaputt.js +0 -1
@@ -1,10 +1,14 @@
1
1
  import { zerlege } from "../argumente.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
4
+ import { KONTEXT_TEXTE } from "../texte/kontext.js";
5
+ import { ratNachUnberuehrt } from "../kontext-rat.js";
2
6
  import { ruf } from "../api.js";
3
7
  import { heuteAus } from "../nachricht.js";
4
8
  import { istUnsichererPfad } from "../arbeitsplatz-dateien.js";
5
- import { LEER, VERWALTETE_DATEIEN, alsMerkzettel, sicherungsOrdner, sicherungsZiel, lageAusMerkzetteln, ratNachUnberuehrt, } from "../kontext-stand.js";
9
+ import { LEER, verwalteteDateien, alsMerkzettel, sicherungsOrdner, sicherungsZiel, lageAusMerkzetteln, } from "../kontext-stand.js";
6
10
  import { leseEineDatei, leseMerkzettelDatei, schreibeMerkzettelDatei, schreibeVerwalteteDatei, sichereDatei, } from "../kontext-dateien.js";
7
- import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js";
11
+ import { ERLAUBT, absage, arbeitsplatzOderAbsage, fremdeSpracheAbsage } from "./kontext-gemeinsam.js";
8
12
  /**
9
13
  * Holt den Firmenkontext von der App und legt ihn in den verwalteten Dateien
10
14
  * des gefundenen Arbeitsplatzes ab -- `CLAUDE.md` und die fuenf Dateien in
@@ -21,7 +25,7 @@ import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js"
21
25
  * Git benutzt.
22
26
  *
23
27
  * ⛔ **Unterhalb von `werkbank-<name>/` wird nichts geschrieben** (Spec §4).
24
- * Dieser Befehl kennt genau die Pfade aus `VERWALTETE_DATEIEN`, und in keinem
28
+ * Dieser Befehl kennt genau die Pfade aus `verwalteteDateien(sprache)`, und in keinem
25
29
  * davon steht ein Personenname.
26
30
  *
27
31
  * 🔁 **Ruling T9-2:** Die Gegenrichtung, `kontextHoch`, wohnt seit dem Split
@@ -29,25 +33,18 @@ import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js"
29
33
  * Firmenwahl-Absage und die `--firma`/`--firma-id`-Optionen ueber
30
34
  * `kontext-gemeinsam.ts`.
31
35
  */
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
- `;
36
+ /** Die Hilfe von `kontext runter` / `context download` in der Sprache des Menschen. */
37
+ export function hilfe(s) {
38
+ return KONTEXT_TEXTE[s].downloadHelp;
39
+ }
45
40
  export async function kontextRunter(argv) {
41
+ let s = sprache();
42
+ let t = KONTEXT_TEXTE[s];
46
43
  if (argv.includes("--help") || argv.includes("-h")) {
47
- console.log(HILFE);
44
+ console.log(hilfe(s));
48
45
  return 0;
49
46
  }
50
- const zerlegt = zerlege(argv, ERLAUBT);
47
+ const zerlegt = zerlege(argv, ERLAUBT, s);
51
48
  if (!zerlegt.ok) {
52
49
  console.error(zerlegt.meldung);
53
50
  return 1;
@@ -66,23 +63,32 @@ export async function kontextRunter(argv) {
66
63
  suche.set("firma", wunsch);
67
64
  if (wunschId !== undefined)
68
65
  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 });
66
+ const pfad = suche.toString() ? `/api/cli/kontext?${suche.toString()}` : "/api/cli/kontext"; // kein-anzeigetext: Pfad der Schnittstelle
67
+ const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true }); // kein-anzeigetext: HTTP-Verb
68
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
69
+ s = sprache();
70
+ t = KONTEXT_TEXTE[s];
71
71
  if (antwort.status !== 200) {
72
- return absage(antwort, "kontext runter");
72
+ return absage(antwort, "kontext runter", ""); // kein-anzeigetext: Befehlsschlüssel
73
73
  }
74
74
  // 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dasselbe Muster wie
75
75
  // in `firma-daten.ts`. `ruf` verschluckt Parse-Fehler zu `null`.
76
76
  if (antwort.koerper === null || typeof antwort.koerper !== "object") {
77
- console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
77
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
78
78
  return 3;
79
79
  }
80
80
  const daten = alsKontextAntwort(antwort.koerper);
81
81
  if (daten === null) {
82
- console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
82
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
83
83
  return 3;
84
84
  }
85
- const lokalerMerkzettel = leseMerkzettelDatei(ordner);
85
+ // 🔴 US-13: Die Firma spricht inzwischen eine andere Sprache als dieser Ordner. VOR der
86
+ // Schleife pruefen -- sonst stuenden nach dem Schreiben zwei Sprachen nebeneinander.
87
+ if (daten.sprache !== ordner.sprache) {
88
+ fremdeSpracheAbsage(ordner.sprache, daten.sprache, daten.firma.name);
89
+ return 1;
90
+ }
91
+ const lokalerMerkzettel = leseMerkzettelDatei(ordner.pfad);
86
92
  const heute = heuteAus(new Date());
87
93
  const geschrieben = [];
88
94
  // Punkt A-1: die Teilmenge von `geschrieben`, bei der NICHT die Firma etwas
@@ -92,23 +98,35 @@ export async function kontextRunter(argv) {
92
98
  const vorlageErneuert = [];
93
99
  const gesicherte = [];
94
100
  const unberuehrt = [];
95
- for (const verwalteterPfad of VERWALTETE_DATEIEN) {
101
+ /*
102
+ 🔴 **ERST ALLES PRUEFEN UND LESEN, DANN SCHREIBEN** (Final-Review, Fund 2).
103
+ Bis dahin lief Pruefen und Schreiben in EINER Schleife: Fehlte in der
104
+ Antwort die vierte Datei, waren `CLAUDE.md` und die ersten drei schon
105
+ ueberschrieben, der Merkzettel aber nicht -- ein halber Stand, den das
106
+ naechste `runter` falsch einordnet. Jetzt bricht eine unvollstaendige
107
+ Lieferung (oder eine unlesbare lokale Datei) ab, bevor ein Byte geschrieben ist.
108
+ */
109
+ const vorbereitet = [];
110
+ for (const verwalteterPfad of verwalteteDateien(ordner.sprache)) {
96
111
  const serverDatei = daten.dateien.find((d) => d.pfad === verwalteterPfad);
97
112
  if (!serverDatei) {
98
- console.error(`Die Antwort enthaelt keine Datei „${verwalteterPfad}". Bitte melden.`);
113
+ console.error(t.answerMissingFile(verwalteterPfad));
99
114
  return 3;
100
115
  }
101
116
  // Zweites Netz -- s. Kommentar an `schreibeVerwalteteDatei`: `pfad` kommt
102
117
  // hier aus der eigenen Iteration, nicht roh vom Server, trotzdem geprueft.
103
118
  if (istUnsichererPfad(serverDatei.pfad)) {
104
- console.error(`Die Antwort enthaelt einen unsicheren Pfad: „${serverDatei.pfad}". Bitte melden.`);
119
+ console.error(t.answerUnsafePath(serverDatei.pfad));
105
120
  return 3;
106
121
  }
107
- const lokalGelesen = leseEineDatei(ordner, verwalteterPfad);
122
+ const lokalGelesen = leseEineDatei(ordner.pfad, verwalteterPfad);
108
123
  if (!lokalGelesen.ok) {
109
124
  console.error(lokalGelesen.meldung);
110
125
  return 1;
111
126
  }
127
+ vorbereitet.push({ pfad: verwalteterPfad, serverInhalt: serverDatei.inhalt, lokalerInhalt: lokalGelesen.inhalt });
128
+ }
129
+ for (const { pfad: verwalteterPfad, serverInhalt, lokalerInhalt } of vorbereitet) {
112
130
  /*
113
131
  🔴 **DIE BEIDEN MERKZETTEL GEHEN ALS GANZES HINEIN -- Punkt A-1,
114
132
  2026-08-26.** Hier standen vorher drei Zeilen, die den Eintrag, den
@@ -125,15 +143,15 @@ export async function kontextRunter(argv) {
125
143
  */
126
144
  const lage = lageAusMerkzetteln({
127
145
  pfad: verwalteterPfad,
128
- lokalerInhalt: lokalGelesen.inhalt,
129
- serverInhalt: serverDatei.inhalt,
146
+ lokalerInhalt,
147
+ serverInhalt,
130
148
  lokal: lokalerMerkzettel,
131
149
  vomServer: daten.merkzettel,
132
150
  });
133
151
  if (lage === "beides") {
134
- // `lokalGelesen.inhalt` ist hier garantiert eine Zeichenkette:
152
+ // `lokalerInhalt` ist hier garantiert eine Zeichenkette:
135
153
  // `lageFuerDatei` liefert "beides" nie, wenn `lokalerInhalt === null`.
136
- const sicherung = sichereDatei(ordner, verwalteterPfad, lokalGelesen.inhalt, heute);
154
+ const sicherung = sichereDatei(ordner.pfad, verwalteterPfad, lokalerInhalt, heute, ordner.sprache);
137
155
  if (!sicherung.ok) {
138
156
  console.error(sicherung.meldung);
139
157
  return 1;
@@ -163,7 +181,7 @@ export async function kontextRunter(argv) {
163
181
  vermisst hat, weil ihn niemand erreichen konnte.
164
182
  */
165
183
  if (lage === "server-neuer" || lage === "beides" || lage === "vorlage-neuer") {
166
- const geschriebenErg = schreibeVerwalteteDatei(ordner, verwalteterPfad, serverDatei.inhalt);
184
+ const geschriebenErg = schreibeVerwalteteDatei(ordner.pfad, verwalteterPfad, serverInhalt, ordner.sprache);
167
185
  if (!geschriebenErg.ok) {
168
186
  console.error(geschriebenErg.meldung);
169
187
  return 1;
@@ -200,7 +218,7 @@ export async function kontextRunter(argv) {
200
218
  dieselbe Sorte Fehlalarm wie eine, die nie ankommt.*
201
219
  */
202
220
  const merkzettelZumSchreiben = { firma_id: daten.firma.id, dateien: daten.merkzettel.dateien };
203
- const merkzettelSchreiben = schreibeMerkzettelDatei(ordner, merkzettelZumSchreiben);
221
+ const merkzettelSchreiben = schreibeMerkzettelDatei(ordner.pfad, merkzettelZumSchreiben);
204
222
  if (!merkzettelSchreiben.ok) {
205
223
  console.error(merkzettelSchreiben.meldung);
206
224
  return 1;
@@ -217,49 +235,46 @@ export async function kontextRunter(argv) {
217
235
  // (vor dem Ergebnis) und nennt den Ordner. Die eigentliche Reihenfolge
218
236
  // (erst sichern, dann ueberschreiben) erzwingt schon der Code oben.
219
237
  if (gesicherte.length > 0) {
220
- console.log(`Ich sichere deine lokale Fassung, bevor ich sie ersetze -- Ordner: ${sicherungsOrdner(heute)}`);
238
+ console.log(t.backupHeader(sicherungsOrdner(ordner.sprache, heute)));
221
239
  for (const g of gesicherte) {
222
240
  if (g.nebenVorhandener) {
223
- const bisherige = sicherungsZiel(heute, g.pfad).split("/").pop();
241
+ const bisherige = sicherungsZiel(ordner.sprache, heute, g.pfad).split("/").pop();
224
242
  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.");
243
+ console.log(t.backupAlreadyToday(g.pfad, bisherige, neue));
228
244
  }
229
245
  else {
230
- console.log(` „${g.pfad}" -> ${g.ziel.split("/").pop()}`);
246
+ console.log(t.backupPlain(g.pfad, g.ziel.split("/").pop()));
231
247
  }
232
248
  }
233
249
  }
234
250
  if (geschrieben.length > 0) {
235
- console.log(`✓ aktualisiert: ${geschrieben.join(", ")}`);
251
+ console.log(t.updated(geschrieben.join(", ")));
236
252
  }
237
253
  // 🔴 Punkt A-1: Eine eigene Zeile, kein stilles Mitschreiben. „Aktualisiert"
238
254
  // heisst fuer den Leser sonst automatisch „meine Firma hat etwas gesagt" --
239
255
  // und dann sucht er in der App eine Aenderung, die es dort nicht gibt.
240
256
  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.");
257
+ console.log(t.fromOurTemplate(vorlageErneuert.join(", ")));
258
+ console.log(t.templateUnchanged);
244
259
  }
245
260
  // §8: Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall --
246
261
  // "nichts geschrieben" heisst hier DREI verschiedene Dinge, und die Ausgabe
247
262
  // muss sie unterscheiden, nicht nur die Rueckgabe (alle drei geben 0).
248
263
  if (unberuehrt.length > 0) {
249
- console.log(`Ich fasse nichts an -- du hast lokal geaendert: ${unberuehrt.join(", ")}`);
264
+ console.log(t.leftAlone(unberuehrt.join(", ")));
250
265
  // 🔴 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)) {
266
+ // `ratNachUnberuehrt` (kontext-rat.ts), Review-Befund F-3.
267
+ for (const zeile of ratNachUnberuehrt(unberuehrt, daten.rolle, daten.adminName, ordner.sprache, s)) {
253
268
  console.log(zeile);
254
269
  }
255
270
  }
256
271
  if (geschrieben.length === 0 && unberuehrt.length === 0) {
257
- const alleLeer = VERWALTETE_DATEIEN.every((p) => (daten.merkzettel.dateien[p]?.stand ?? LEER) === LEER);
272
+ const alleLeer = verwalteteDateien(ordner.sprache).every((p) => (daten.merkzettel.dateien[p]?.stand ?? LEER) === LEER);
258
273
  if (alleLeer) {
259
- console.log("Deine Firma hat noch nichts hinterlegt. Bis dahin bleiben die Vordrucke stehen.");
274
+ console.log(t.companyEmpty);
260
275
  }
261
276
  else {
262
- console.log("Dein Firmenkontext ist aktuell.");
277
+ console.log(t.upToDate);
263
278
  }
264
279
  }
265
280
  return 0;
@@ -277,6 +292,8 @@ function alsKontextAntwort(koerper) {
277
292
  const firmaId = firmaRoh.id;
278
293
  if (typeof firmaId !== "string" || firmaId.trim() === "")
279
294
  return null;
295
+ const firmaNameRoh = firmaRoh.name;
296
+ const firmaName = typeof firmaNameRoh === "string" && firmaNameRoh.trim() !== "" ? firmaNameRoh : null;
280
297
  const rolle = k.rolle;
281
298
  if (rolle !== "admin" && rolle !== "member")
282
299
  return null;
@@ -301,5 +318,7 @@ function alsKontextAntwort(koerper) {
301
318
  const merkzettel = alsMerkzettel(k.merkzettel);
302
319
  if (merkzettel === null)
303
320
  return null;
304
- return { firma: { id: firmaId }, rolle, adminName, dateien, merkzettel };
321
+ // 🌐 Teil 2: die Firmensprache. Fehlt das Feld, antwortet ein Server vor 0.3.0 -- der kennt nur Deutsch.
322
+ const sprache = k.sprache === "en" ? "en" : "de";
323
+ return { firma: { id: firmaId, name: firmaName }, rolle, adminName, dateien, merkzettel, sprache };
305
324
  }
@@ -1,18 +1,24 @@
1
1
  import { hostname } from "node:os";
2
2
  import { feld, ruf } from "../api.js";
3
3
  import { BASIS_URL, schreibeAnmeldung } from "../anmeldung.js";
4
+ import { sprache } from "../sprache.js";
5
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
6
+ import { stelleSkillUm } from "./skill-tausch.js";
4
7
  const warte = (ms) => new Promise((f) => setTimeout(f, ms));
5
8
  export async function login() {
9
+ // 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
10
+ // (Task 5) und kann sie mitten im Lauf aendern -- die Adresse vorher kam noch in der alten.
11
+ const t = () => ALLGEMEIN_TEXTE[sprache()];
6
12
  const start = await ruf("/api/cli/login/start", { koerper: { bezeichnung: hostname() } });
7
13
  const nutzerCode = feld(start.koerper, "nutzer_code");
8
14
  const geraeteCode = feld(start.koerper, "geraete_code");
9
15
  if (start.status !== 200 || !nutzerCode || !geraeteCode) {
10
- console.error("kimal3 ist gerade nicht erreichbar.");
16
+ console.error(t().unreachable);
11
17
  return 3;
12
18
  }
13
- console.log(`\n Öffne ${BASIS_URL}/cli`);
14
- console.log(` und gib diesen Code ein: ${nutzerCode}\n`);
15
- console.log(" Warte auf die Bestätigung … (Abbruch mit Strg+C)");
19
+ console.log(t().loginOpen(BASIS_URL));
20
+ console.log(t().loginEnterCode(nutzerCode));
21
+ console.log(t().loginWaiting);
16
22
  const endeUm = Date.now() + 10 * 60 * 1000;
17
23
  while (Date.now() < endeUm) {
18
24
  await warte(2000);
@@ -34,7 +40,7 @@ export async function login() {
34
40
  if (a.status >= 500)
35
41
  continue;
36
42
  if (a.status === 410) {
37
- console.error("\nDer Anmeldecode ist abgelaufen. Bitte `kimal3 login` noch einmal.");
43
+ console.error(t().loginExpired);
38
44
  return 2;
39
45
  }
40
46
  // 🔴 Dieser Zweig MUSS hier oben stehen. Ohne ihn faellt eine 402 durch bis
@@ -48,19 +54,27 @@ export async function login() {
48
54
  // Schleife fragt also einfach weiter, statt aufzugeben. Genau dafuer ist
49
55
  // sie da. (Review-Befund 2026-08-14, s. `src/app/cli/actions.ts`.)
50
56
  if (a.status === 402) {
51
- console.error(`\n${feld(a.koerper, "meldung") ?? "Dafür brauchst du ein Pro-Konto."}`);
57
+ console.error(`\n${feld(a.koerper, "meldung") ?? t().needsPro}`);
52
58
  return 4;
53
59
  }
54
60
  const schluessel = feld(a.koerper, "schluessel");
55
61
  const email = feld(a.koerper, "email");
56
62
  if (a.status === 200 && schluessel && email) {
57
63
  schreibeAnmeldung({ schluessel, email });
58
- console.log(`\n✓ Angemeldet als ${email}.`);
64
+ console.log(t().signedInAs(email));
65
+ // 🌐 Ab hier ist die Kontosprache bekannt (die Abhol-Antwort nennt sie). Liegt Claudes Anleitung in der
66
+ // anderen Sprache -- `skill install` lief auf /pro VOR dem Login --, stellt der Login sie um. Der Login
67
+ // bleibt gelungen, was immer dabei passiert (Lagen-Tabelle in docs/qa/2026-10-02-mehrsprachig-teil-4-messungen.md).
68
+ const umstellung = stelleSkillUm(sprache());
69
+ if (umstellung === "umgestellt")
70
+ console.log(t().skillSwitched);
71
+ if (umstellung === "unvollstaendig")
72
+ console.error(t().skillSwitchIncomplete);
59
73
  return 0;
60
74
  }
61
- console.error("\nDas hat nicht geklappt.");
75
+ console.error(t().loginFailed);
62
76
  return 3;
63
77
  }
64
- console.error("\nDer Anmeldecode ist abgelaufen. Bitte `kimal3 login` noch einmal.");
78
+ console.error(t().loginExpired);
65
79
  return 2;
66
80
  }
@@ -1,5 +1,7 @@
1
1
  import { leseAnmeldung, loescheAnmeldung } from "../anmeldung.js";
2
2
  import { ruf } from "../api.js";
3
+ import { sprache } from "../sprache.js";
4
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
3
5
  /**
4
6
  * Abmelden — erst beim Server widerrufen, DANN die Datei loeschen.
5
7
  *
@@ -8,26 +10,29 @@ import { ruf } from "../api.js";
8
10
  * Server gueltig, und nichts wuerde davon berichten.
9
11
  */
10
12
  export async function logout() {
13
+ let t = ALLGEMEIN_TEXTE[sprache()];
11
14
  const a = leseAnmeldung();
12
15
  if (!a) {
13
16
  // Kein Fehler: Der Zielzustand ist bereits erreicht. Deshalb 0.
14
- console.log("Auf diesem Rechner war keine Anmeldung.");
17
+ console.log(t.noSignIn);
15
18
  return 0;
16
19
  }
17
20
  try {
18
21
  const antwort = await ruf("/api/cli/logout", { mitAusweis: true });
22
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
23
+ t = ALLGEMEIN_TEXTE[sprache()];
19
24
  if (antwort.status !== 200 && antwort.status !== 401) {
20
- console.error("kimal3 ist gerade nicht erreichbar. Es wurde nichts geändert.");
25
+ console.error(t.unreachableNothingChanged);
21
26
  return 3;
22
27
  }
23
28
  // 401 heisst: Der Schluessel gilt ohnehin nicht mehr (schon widerrufen
24
29
  // oder abgelaufen). Der Zielzustand stimmt also — die Datei darf weg.
25
30
  }
26
31
  catch {
27
- console.error("kimal3 ist gerade nicht erreichbar. Es wurde nichts geändert.");
32
+ console.error(t.unreachableNothingChanged);
28
33
  return 3;
29
34
  }
30
35
  loescheAnmeldung();
31
- console.log(`Abgemeldet. (${a.email})`);
36
+ console.log(t.signedOut(a.email));
32
37
  return 0;
33
38
  }
@@ -1,6 +1,10 @@
1
1
  import { feld, ruf } from "../api.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { befehlIn, schalterIn } from "../befehlswoerter.js";
4
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
5
+ import { PROJEKTE_TEXTE } from "../texte/projekte.js";
2
6
  import { schlussFehler } from "../fehler.js";
3
- import { MAX_PROJEKTE_ANZEIGE, antwortHatProjekteFeld, titelFeldLesen, projekteAusAntwort, projektZeilen, titelAusAlterAntwort, } from "../projekt-zeilen.js";
7
+ import { MAX_PROJEKTE_ANZEIGE, antwortHatProjekteFeld, titelFeldLesen, projekteAusAntwort, projektZeilen, titelAusAlterAntwort, titelListeZeilen, werkzeugZuAltZeilen, } from "../projekt-zeilen.js";
4
8
  import { BASIS_URL } from "../anmeldung.js";
5
9
  /**
6
10
  * Die eigenen Projekte anzeigen.
@@ -15,7 +19,10 @@ import { BASIS_URL } from "../anmeldung.js";
15
19
  * Werkzeug hergibt, laesst Claude entweder raten oder sich entschuldigen.*
16
20
  */
17
21
  export async function projektListe() {
18
- const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true });
22
+ const antwort = await ruf("/api/cli/projekt", { methode: "GET", mitAusweis: true }); // kein-anzeigetext: HTTP-Methode
23
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
24
+ const s = sprache();
25
+ const t = PROJEKTE_TEXTE[s];
19
26
  if (antwort.status === 200) {
20
27
  // 🔴 Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dieselbe Wache wie
21
28
  // in `firma-daten.ts`: `ruf` verschluckt Parse-Fehler zu `null`, und ein
@@ -43,16 +50,11 @@ export async function projektListe() {
43
50
  const alt = titelAusAlterAntwort(antwort.koerper);
44
51
  if (alt) {
45
52
  if (alt.length === 0) {
46
- console.log("Du hast noch keine Projekte eingetragen.");
47
- console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
53
+ console.log(t.noProjects);
54
+ console.log(t.addHint(befehlIn("projekt neu", s), schalterIn("titel", s))); // kein-anzeigetext: Befehlsschluessel
48
55
  return 0;
49
56
  }
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`);
57
+ titelListeZeilen(alt, "alt", s, BASIS_URL).forEach((z) => console.log(z));
56
58
  return 0;
57
59
  }
58
60
  /*
@@ -75,35 +77,28 @@ export async function projektListe() {
75
77
  */
76
78
  const titel = titelFeldLesen(antwort.koerper);
77
79
  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`);
80
+ titelListeZeilen(titel, "neu", s, BASIS_URL).forEach((z) => console.log(z));
84
81
  return 0;
85
82
  }
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");
83
+ werkzeugZuAltZeilen(s).forEach((z) => console.error(z));
89
84
  return 1;
90
85
  }
91
- console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
86
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
92
87
  return 3;
93
88
  }
94
89
  // Eine leere Liste ist KEIN Fehler — sie ist eine gueltige Antwort auf
95
90
  // eine gueltige Frage. Deshalb 0 und ein Satz, kein Fehlertext.
96
91
  if (projekte.length === 0) {
97
- console.log("Du hast noch keine Projekte eingetragen.");
98
- console.log(` Eintragen: kimal3 projekt neu --titel "…"`);
92
+ console.log(t.noProjects);
93
+ console.log(t.addHint(befehlIn("projekt neu", s), schalterIn("titel", s))); // kein-anzeigetext: Befehlsschluessel
99
94
  return 0;
100
95
  }
101
- console.log(`\n Deine Projekte (${projekte.length} von ${MAX_PROJEKTE_ANZEIGE}):\n`);
102
- projektZeilen(projekte).forEach((z) => console.log(z));
103
- console.log(`\n Ansehen: ${BASIS_URL}/profil\n`);
96
+ console.log(t.projectsHeaderBlank(projekte.length, MAX_PROJEKTE_ANZEIGE));
97
+ projektZeilen(projekte, s).forEach((z) => console.log(z));
98
+ console.log(t.viewOnline(BASIS_URL));
104
99
  return 0;
105
100
  }
106
- const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
101
+ const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
107
102
  // 402 vor 401: Beide heissen "kommst hier nicht rein", verlangen aber
108
103
  // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` —
109
104
  // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
@@ -112,11 +107,11 @@ export async function projektListe() {
112
107
  return 4;
113
108
  }
114
109
  if (antwort.status === 401) {
115
- console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
110
+ console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
116
111
  return 2;
117
112
  }
118
113
  if (antwort.status === 409 && feld(antwort.koerper, "grund") === "kein-profil") {
119
- console.error(`${meldung} Leg es hier an: ${BASIS_URL}/profil`);
114
+ console.error(`${meldung}${t.createProfile(BASIS_URL)}`);
120
115
  return 1;
121
116
  }
122
117
  // ⚠️ Hier landet auch die `503` "Konto gerade nicht prüfbar" — und sie MUSS
@@ -128,7 +123,7 @@ export async function projektListe() {
128
123
  // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
129
124
  // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
130
125
  // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
131
- const schluss = schlussFehler(antwort.status, meldung);
126
+ const schluss = schlussFehler(antwort.status, meldung, s);
132
127
  console.error(schluss.text);
133
128
  return schluss.wert;
134
129
  }
@@ -1,4 +1,7 @@
1
1
  import { feld, ruf } from "../api.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
4
+ import { PROJEKTE_TEXTE } from "../texte/projekte.js";
2
5
  import { schlussFehler } from "../fehler.js";
3
6
  import { BASIS_URL } from "../anmeldung.js";
4
7
  import { zerlege } from "../argumente.js";
@@ -8,34 +11,22 @@ const OPTIONEN = [
8
11
  "titel", "beschreibung", "symbol", "stunden", "ergebnis", "umsatz",
9
12
  "wirkung", "wirkung-label", "link", "live-seit", "firma", "firma-id",
10
13
  ];
11
- // exportiert, damit `check:cli` die HILFE als AUSGABE-Text prueft (§24
12
- // erlaubt das: nicht die Anleitung, die den Text beschreibt, sondern der
13
- // Text, der wirklich auf dem Bildschirm des Kunden landet).
14
- export const HILFE = `
15
- kimal3 projekt neu --titel "Angebots-Generator" [Optionen]
16
-
17
- --titel Pflicht. 2–80 Zeichen.
18
- --beschreibung Bis 300 Zeichen.
19
- --symbol Eines der bekannten Symbole, z. B. bot, mail, workflow.
20
- --stunden Zeitersparnis pro Woche, 0–168.
21
- --ergebnis Ergebnis pro Monat in Euro, z. B. 1200 oder 1200,50.
22
- --umsatz Umsatz pro Monat in Euro.
23
- --wirkung Kurz, bis 20 Zeichen, z. B. "-70%".
24
- --wirkung-label Wozu, bis 40 Zeichen, z. B. "Angebotszeit".
25
- --link Vollstaendige Adresse mit http:// oder https://.
26
- --live-seit Datum als JJJJ-MM-TT.
27
- --firma "Name" Wenn du zu mehreren Firmen gehoerst: welche das Projekt bekommt.
28
- --firma-id "…" Falls zwei deiner Firmen gleich heissen.
29
- --ohne-firma Das Projekt gehoert keiner Firma (steht nur auf deinem Profil).
30
- `;
14
+ // exportiert, damit `check:cli` die Hilfe als AUSGABE-Text prueft (§24 erlaubt das: nicht die
15
+ // Anleitung, die den Text beschreibt, sondern der Text, der wirklich auf dem Bildschirm des Kunden
16
+ // landet).
17
+ export function hilfe(s) {
18
+ return PROJEKTE_TEXTE[s].newHelp;
19
+ }
31
20
  export async function projektNeu(argv) {
21
+ let s = sprache();
22
+ let t = PROJEKTE_TEXTE[s];
32
23
  // ⚠️ VOR dem Zerlegen abfragen, nicht danach.
33
24
  // Der Bauplan hatte "help" in der Optionsliste und fragte hinterher. Das
34
25
  // konnte nie greifen: `zerlege` verlangt zu jeder Option einen Wert, also
35
26
  // scheitert `--help` schon dort mit "fehlt ihr Wert". Ein Schalter ist keine
36
27
  // Option mit Wert — er gehoert vor den Zerleger.
37
28
  if (argv.includes("--help") || argv.includes("-h")) {
38
- console.log(HILFE);
29
+ console.log(hilfe(s));
39
30
  return 0;
40
31
  }
41
32
  // ⚠️ `zerlege` kennt keine Optionen ohne Wert -- `--ohne-firma` wird
@@ -43,21 +34,21 @@ export async function projektNeu(argv) {
43
34
  // in kontext-hoch.ts. Die Rechnung dahinter steht in projekt-neu-firma.ts,
44
35
  // damit `check:cli` sie pruefen kann (Final-Review F-1).
45
36
  const { rest, ohneFirma } = ohneFirmaAusArgv(argv);
46
- const z = zerlege(rest, OPTIONEN);
37
+ const z = zerlege(rest, OPTIONEN, s);
47
38
  if (!z.ok) {
48
39
  console.error(z.meldung);
49
40
  return 1;
50
41
  }
51
42
  const o = z.werte.optionen;
52
43
  if (!o.titel) {
53
- console.error("Es fehlt --titel.\n" + HILFE);
44
+ console.error(t.missingTitle + hilfe(s));
54
45
  return 1;
55
46
  }
56
47
  // 🔴 Final-Review F-3: `--ohne-firma` zusammen mit `--firma`/`--firma-id`
57
48
  // liesse die Route den Schalter still gewinnen lassen (`firmenWunschAus` in
58
49
  // route.ts prueft `ohne_firma` zuerst). Hier abweisen, bevor der Wunsch
59
50
  // ueberhaupt losgeschickt wird.
60
- const wunsch = firmenKoerperAus(o.firma, o["firma-id"], ohneFirma);
51
+ const wunsch = firmenKoerperAus(o.firma, o["firma-id"], ohneFirma, s);
61
52
  if (!wunsch.ok) {
62
53
  console.error(wunsch.meldung);
63
54
  return 1;
@@ -73,14 +64,17 @@ export async function projektNeu(argv) {
73
64
  ...wunsch.koerper,
74
65
  },
75
66
  });
67
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben (Spec 3.2, Schritt 3): Was ab hier gedruckt wird, in der neuen.
68
+ s = sprache();
69
+ t = PROJEKTE_TEXTE[s];
76
70
  if (antwort.status === 201) {
77
71
  const titel = feld(antwort.koerper, "titel") ?? o.titel;
78
72
  const anzahl = antwort.koerper?.anzahl;
79
- console.log(`✓ „${titel}" ins Erfolgsprofil eingetragen (Projekt ${anzahl} von 12).`);
80
- console.log(` Ansehen: ${BASIS_URL}/profil`);
73
+ console.log(t.projectAdded(titel, anzahl));
74
+ console.log(t.viewOnlineShort(BASIS_URL));
81
75
  return 0;
82
76
  }
83
- const meldung = feld(antwort.koerper, "meldung") ?? "Das hat nicht geklappt.";
77
+ const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
84
78
  // 402 vor 401: Beide heissen "kommst hier nicht rein", verlangen aber
85
79
  // Gegenteiliges. Wer sie zusammenwirft, raet dem Menschen `kimal3 login` —
86
80
  // und der meldet sich dann erfolgreich an, ohne dass es etwas aendert.
@@ -89,11 +83,11 @@ export async function projektNeu(argv) {
89
83
  return 4;
90
84
  }
91
85
  if (antwort.status === 401) {
92
- console.error(`${meldung} Bitte \`kimal3 login\` ausführen.`);
86
+ console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
93
87
  return 2;
94
88
  }
95
89
  if (antwort.status === 409 && feld(antwort.koerper, "grund") === "kein-profil") {
96
- console.error(`${meldung} Leg es hier an: ${BASIS_URL}/profil`);
90
+ console.error(`${meldung}${t.createProfile(BASIS_URL)}`);
97
91
  return 1;
98
92
  }
99
93
  if (antwort.status === 409) {
@@ -101,7 +95,7 @@ export async function projektNeu(argv) {
101
95
  // Meldung und die Firmen als fertige Befehle. Der Befehl in der Zeile
102
96
  // traegt ALLE getippten Optionen mit, nicht nur --titel (Final-Review
103
97
  // F-2) -- sonst verliert der Kunde beim Kopieren --stunden, --ergebnis &co.
104
- return absage(antwort, `projekt neu ${befehlszeileOhneFirma(o)}`);
98
+ return absage(antwort, "projekt neu", ` ${befehlszeileOhneFirma(o, s)}`); // kein-anzeigetext: Befehlsschluessel
105
99
  }
106
100
  // ⚠️ Hier landet auch die `503` "Konto gerade nicht prüfbar" — und sie MUSS
107
101
  // hier landen, nicht oben bei der 402. Rueckgabewert 3 heisst "nochmal
@@ -112,7 +106,7 @@ export async function projektNeu(argv) {
112
106
  // 🔑 Den Rest entscheidet `schlussFehler` fuer alle Befehle gemeinsam --
113
107
  // insbesondere die `404` (B-7), die hier frueher auf die `1` fiel und damit
114
108
  // "Eingabe passt nicht" meldete, obwohl die Eingabe stimmte.
115
- const schluss = schlussFehler(antwort.status, meldung);
109
+ const schluss = schlussFehler(antwort.status, meldung, s);
116
110
  console.error(schluss.text);
117
111
  return schluss.wert;
118
112
  }