kimal3 0.2.10 → 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 (66) hide show
  1. package/README.md +135 -0
  2. package/dist/api.js +16 -7
  3. package/dist/arbeitsplatz-dateien.js +23 -19
  4. package/dist/argumente.js +25 -97
  5. package/dist/befehle/arbeitsplatz.js +50 -68
  6. package/dist/befehle/firma-daten.js +24 -26
  7. package/dist/befehle/kontext-fragen.js +57 -47
  8. package/dist/befehle/kontext-gemeinsam.js +84 -40
  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 +98 -87
  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 +8 -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 +64 -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/werkbank-name.js +28 -10
  49. package/dist/wort.js +98 -0
  50. package/package.json +2 -2
  51. package/skill/SKILL.md +9 -9
  52. package/skill/commands/k3.md +1 -1
  53. package/skill/commands/k3feierabend.md +99 -97
  54. package/skill/commands/k3kontext.md +1 -1
  55. package/skill/commands/k3kurs.md +6 -5
  56. package/skill/commands/k3start.md +2 -1
  57. package/skill/en/SKILL.md +451 -0
  58. package/skill/en/commands/k3.md +106 -0
  59. package/skill/en/commands/k3analyse.md +59 -0
  60. package/skill/en/commands/k3context.md +92 -0
  61. package/skill/en/commands/k3course.md +54 -0
  62. package/skill/en/commands/k3list.md +26 -0
  63. package/skill/en/commands/k3project.md +50 -0
  64. package/skill/en/commands/k3start.md +47 -0
  65. package/skill/en/commands/k3wrapup.md +99 -0
  66. package/dist/zz-kaputt.js +0 -1
package/README.md CHANGED
@@ -135,3 +135,138 @@ Proprietär. Die Nutzung ist Inhabern eines gültigen KImal3-Pro-Kontos gestatte
135
135
  Weitergabe, Veränderung und Weiterverwendung des Codes sind nicht erlaubt.
136
136
 
137
137
  © KImal3 · https://kimal3.de
138
+
139
+ ---
140
+
141
+ # kimal3 (English)
142
+
143
+ **Log your AI projects where they happen: in the terminal.**
144
+
145
+ If you work with Claude Code, you build real things — and then never write them up, because that would be an extra step in another window. `kimal3` closes that gap: at the end
146
+ of a session, Claude adds the finished project to your KImal3 success profile itself. All
147
+ you do is approve it there.
148
+
149
+ > **Requirement: a KImal3 account with Pro.**
150
+ > Without Pro, this tool tells you so and does nothing else.
151
+ > What Pro includes and how to get it: **https://app.kimal3.de/pro**
152
+
153
+ ## Getting started
154
+
155
+ Once per computer:
156
+
157
+ ```bash
158
+ npm install -g kimal3 # install the tool (needs Node.js)
159
+ kimal3 login # connect this computer to your account
160
+ kimal3 skill install # teach Claude to use the tool — and get the /k3 commands
161
+ ```
162
+
163
+ > **In Claude Code?** Then have it set this up for you instead of typing it — the text to copy is on **https://app.kimal3.de/pro**. The only thing you type yourself is `kimal3 login` (with a `!` in front, there), because confirming it in the browser needs a person.
164
+
165
+ `login` shows you an address and an eight-character code. You open the address yourself and
166
+ confirm the code there, in your signed-in account — just like `gh auth login`.
167
+ **No password is ever typed into the terminal.**
168
+
169
+ ## Commands
170
+
171
+ ```
172
+ kimal3 Shows whether this computer is signed in
173
+ kimal3 login Connect this computer to your account
174
+ kimal3 skill install Teach Claude to use this tool — and give you the /k3 commands
175
+ kimal3 workplace new Create the whole AI Workplace — company level and workbench
176
+ kimal3 workbench new Only create your own workbench in an existing company
177
+ kimal3 context download Fetch the company context — CLAUDE.md and the five files in context/
178
+ kimal3 context questions Answer the questions about your company — Claude takes you through them
179
+ kimal3 context upload Upload your local changes to the five company texts
180
+ kimal3 project new … Add a project to your success profile
181
+ kimal3 project list Show the projects you've added
182
+ kimal3 company data Fetch your company's data so Claude can analyse it
183
+ kimal3 watch Runs by itself when Claude starts: checks your access, messages, version and skill
184
+ kimal3 watch off Switch the daily check off — it then says nothing at all
185
+ kimal3 watch on Switch it back on
186
+ kimal3 logout Disconnect this computer
187
+ ```
188
+
189
+ The German command names in the section above work too.
190
+
191
+ More about one command: `kimal3 project new --help`
192
+
193
+ ## What gets recorded
194
+
195
+ Only what you enter — title, description, optionally a link. **Your code is not read,
196
+ not uploaded and not analysed.** The tool only writes through the KImal3 app and has
197
+ no database access of its own.
198
+
199
+ Projects you add are **not public** at first. They appear in your success profile, and you
200
+ decide there what becomes visible.
201
+
202
+ ## Exit codes
203
+
204
+ For scripts and for Claude itself:
205
+
206
+ | Value | Meaning | What helps |
207
+ |---|---|---|
208
+ | `0` | it worked | — |
209
+ | `1` | input incomplete, nothing found — or your Claude settings can't be read | read the message, it names the case |
210
+ | `2` | not signed in | `kimal3 login` |
211
+ | `3` | can't be reached right now | try again later |
212
+ | `4` | no Pro account | trying again does **not** help |
213
+
214
+ The difference between `3` and `4` is deliberate: with `3` a second attempt is worth it,
215
+ with `4` it never is.
216
+
217
+ ## Update notices
218
+
219
+ If a newer version is available, `kimal3` writes one indented line after the command — at
220
+ most **once a day**:
221
+
222
+ ```
223
+ → There's a new version of kimal3. npm i -g kimal3@latest && kimal3 skill install
224
+ ```
225
+
226
+ It goes to **stderr**, not stdout: `kimal3 company data > company.json` stays clean JSON.
227
+ It **doesn't change the exit code** — a `0` stays a `0`. And `kimal3` **never installs
228
+ anything by itself**; the line shows the command; running it is up to you.
229
+
230
+ The same goes for the skill files: if the ones in `~/.claude/skills/kimal3/` are older than
231
+ the installed tool, or in a different language from your account, `kimal3` says so — this
232
+ works without an internet connection.
233
+
234
+ ## The daily check — and how to switch it off
235
+
236
+ `kimal3 skill install` adds a line to your Claude settings (`~/.claude/settings.json`). It
237
+ makes `kimal3 watch` run when Claude starts — silent when nothing is pending; an open task
238
+ (access, version, skill) is reported in every session until it's done. The check looks at
239
+ whether your access is still valid, whether a newer version is available, whether your `/k3`
240
+ commands are out of date and whether there is a message from us.
241
+
242
+ **It only speaks up when something is really pending.** If everything is in order, it says
243
+ nothing.
244
+
245
+ If you don't want it:
246
+
247
+ ```
248
+ kimal3 watch off # silent from now on
249
+ kimal3 watch on # back on again
250
+ ```
251
+
252
+ After `kimal3 watch off`, **nothing is sent to our server any more** — the check stops before it asks our server anything. Measured: about 0.9 seconds with the check, about 0.19 seconds without, and in
253
+ those 0.19 seconds not a single byte leaves your computer.
254
+
255
+ The line in the settings stays, so that `kimal3 watch on` takes effect immediately and we
256
+ don't have to touch your settings file again. So a small program of ours still starts, and
257
+ stops again after about 0.19 seconds.
258
+
259
+ **If you don't want even that**, remove the line yourself: in `~/.claude/settings.json`,
260
+ under `hooks` → `SessionStart`, there is an entry whose command starts with `kimal3 wache` —
261
+ today `kimal3 wache --hook`, in older installations without `--hook`. ⚠️ **The `--hook` part belongs in the settings file, not in the terminal** — all you ever type is
262
+ `kimal3 watch`. Delete this one entry — leave the rest of the file as it is.
263
+
264
+ ## Requirements
265
+
266
+ Node.js 20 or newer. Nothing else — the tool comes with **no third-party libraries**.
267
+
268
+ ## Licence
269
+
270
+ Proprietary. Use is permitted for holders of a valid KImal3 Pro account; redistributing, modifying or reusing the code is not permitted.
271
+
272
+ © KImal3 · https://kimal3.de
package/dist/api.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
2
2
  import { hinweisAusAntwort } from "./nachricht.js";
3
3
  import { eigeneVersion } from "./version.js";
4
+ import { KONTO_KOPF, SPRACH_KOPF, sprache, uebernimmKontosprache } from "./sprache.js";
4
5
  import { findeArbeitsplatz, leseMerkzettelDatei } from "./kontext-dateien.js";
5
6
  import { bekannterStandAus, istKopfwertSicher } from "./kontext-stand.js";
6
7
  export class NichtAngemeldet extends Error {
7
- constructor() { super("nicht angemeldet"); }
8
+ constructor() { super("nicht angemeldet"); } // kein-anzeigetext: Fehlername, kimal3.ts faengt ihn ab und druckt ihn nie
8
9
  }
9
10
  /**
10
11
  * Der EINE Weg, auf dem diese Datei den lokalen Merkzettel liest -- Firmen-ID
@@ -48,13 +49,13 @@ export class NichtAngemeldet extends Error {
48
49
  */
49
50
  function lokaleKontextLage() {
50
51
  try {
51
- const ordner = findeArbeitsplatz(process.cwd());
52
- if (ordner === null)
52
+ const fund = findeArbeitsplatz(process.cwd());
53
+ if (fund.art !== "gefunden")
53
54
  return null;
54
- const merkzettel = leseMerkzettelDatei(ordner);
55
+ const merkzettel = leseMerkzettelDatei(fund.pfad);
55
56
  if (merkzettel.firma_id === null)
56
57
  return null;
57
- return { firmaId: merkzettel.firma_id, stand: bekannterStandAus(merkzettel) };
58
+ return { firmaId: merkzettel.firma_id, stand: bekannterStandAus(merkzettel, fund.sprache) };
58
59
  }
59
60
  catch {
60
61
  return null;
@@ -106,7 +107,7 @@ export async function ruf(pfad, optionen = {}) {
106
107
  const a = leseAnmeldung();
107
108
  if (!a)
108
109
  throw new NichtAngemeldet();
109
- kopf.Authorization = `Bearer ${a.schluessel}`;
110
+ kopf.Authorization = `Bearer ${a.schluessel}`; // kein-anzeigetext: HTTP-Kopf
110
111
  }
111
112
  /*
112
113
  🔴 DIE CLI SAGT, WER SIE IST -- und nur daran haengt der Server den
@@ -128,6 +129,12 @@ export async function ruf(pfad, optionen = {}) {
128
129
  const version = eigeneVersion();
129
130
  if (version !== null)
130
131
  kopf["X-Kimal3-Version"] = version;
132
+ /*
133
+ 🌐 Teil 3 (T3-6): Die Sprache, in der dieses Programm gerade spricht, reist bei JEDEM Aufruf
134
+ mit. Der Server antwortet dann in der Kontosprache -- oder, wenn er den Menschen nicht kennt,
135
+ in dieser. Fehlt der Kopf (jede aeltere Fassung), antwortet er deutsch wie bisher.
136
+ */
137
+ kopf[SPRACH_KOPF] = sprache();
131
138
  /*
132
139
  🔴 DER ZWEITE KOPF (Task 10) -- der lokal bekannte Firmenstand, damit der
133
140
  Server einen firmenbezogenen Hinweis anhaengen kann ("deine Firma hat den
@@ -152,7 +159,7 @@ export async function ruf(pfad, optionen = {}) {
152
159
  const kontextKopf = kontextStandKopf();
153
160
  if (kontextKopf !== null)
154
161
  kopf["X-Kimal3-Kontext-Stand"] = kontextKopf;
155
- const methode = optionen.methode ?? "POST";
162
+ const methode = optionen.methode ?? "POST"; // kein-anzeigetext: HTTP-Methode
156
163
  // Content-Type nur, wenn wirklich etwas im Rumpf steht. Ein GET mit
157
164
  // "Content-Type: application/json" und ohne Rumpf ist eine Luege ueber die
158
165
  // eigene Anfrage — manche Server nehmen das uebel.
@@ -163,6 +170,8 @@ export async function ruf(pfad, optionen = {}) {
163
170
  headers: kopf,
164
171
  body: methode === "POST" ? JSON.stringify(optionen.koerper ?? {}) : undefined,
165
172
  });
173
+ // 🌐 Nennt die Antwort eine Kontosprache, gilt sie ab sofort (Spec 3.2, Schritt 3).
174
+ uebernimmKontosprache(antwort.headers.get(KONTO_KOPF));
166
175
  const koerper = await antwort.json().catch(() => null);
167
176
  merkeHinweis(koerper);
168
177
  return { status: antwort.status, koerper };
@@ -32,6 +32,9 @@ import { angeboteneFirmen, firmaAusOrdner } from "./firmen-liste.js";
32
32
  statt weg-behauptet zu werden.
33
33
  */
34
34
  import { absage } from "./befehle/kontext-gemeinsam.js";
35
+ import { sprache } from "./sprache.js";
36
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
37
+ import { ARBEITSPLATZ_TEXTE } from "./texte/arbeitsplatz.js";
35
38
  /*
36
39
  🔁 Die Ableitung „welcher Unterordner ist die Werkbank" wohnt seit dem
37
40
  2026-08-20 in `werkbank-name.ts` -- gemeinsam mit der App, die sie fuer `/pro`
@@ -95,7 +98,7 @@ export function firmenSuche(wunsch) {
95
98
  * - Scheitert die zweite Anfrage mit `409`, sieht er die ERSTE Rückfrage, nicht
96
99
  * eine über einen Namen, den er nie getippt hat (L11).
97
100
  */
98
- export async function vorlageHolen(befehl, wunsch, ordner) {
101
+ export async function vorlageHolen(befehl, optionen, wunsch, ordner) {
99
102
  const antwort = await holeVorlage(wunsch);
100
103
  if (antwort.status === 200)
101
104
  return lesbar(antwort.koerper, false);
@@ -111,7 +114,7 @@ export async function vorlageHolen(befehl, wunsch, ordner) {
111
114
  // Netz, Ausweis, Pro: deren eigene Meldung. Nur eine zweite Rückfrage wird
112
115
  // durch die erste ersetzt (L11).
113
116
  if (zweite.status !== 409)
114
- return { ok: false, wert: absage(zweite, befehl) };
117
+ return { ok: false, wert: absage(zweite, befehl, optionen) };
115
118
  }
116
119
  /*
117
120
  🔴 **Hier standen dieselben vier Zweige noch einmal abgetippt** (402 vor
@@ -124,15 +127,15 @@ export async function vorlageHolen(befehl, wunsch, ordner) {
124
127
  *Ein Zweig, den es nie zu sehen gab, fehlt niemandem, bis der Server
125
128
  anfängt zu fragen.*
126
129
  */
127
- return { ok: false, wert: absage(antwort, befehl) };
130
+ return { ok: false, wert: absage(antwort, befehl, optionen) };
128
131
  }
129
132
  function holeVorlage(wunsch) {
130
- return ruf(`/api/cli/arbeitsplatz${firmenSuche(wunsch)}`, { methode: "GET", mitAusweis: true });
133
+ return ruf(`/api/cli/arbeitsplatz${firmenSuche(wunsch)}`, { methode: "GET", mitAusweis: true }); // kein-anzeigetext: Pfad der Schnittstelle und HTTP-Verb
131
134
  }
132
135
  function lesbar(koerper, uebernommen) {
133
136
  const daten = alsVorlageAntwort(koerper);
134
137
  if (daten === null) {
135
- console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
138
+ console.error(ALLGEMEIN_TEXTE[sprache()].unreadableAnswer);
136
139
  return { ok: false, wert: 3 };
137
140
  }
138
141
  return { ok: true, daten, uebernommen };
@@ -205,9 +208,10 @@ export function dateienSchreiben(dateien, ordnerName, hinweis) {
205
208
  // Dateien -- sonst meldet der Befehl "✓ 0 Dateien angelegt." und legt dabei
206
209
  // nicht einmal einen Ordner an. Derselbe Rueckgabewert wie bei anderen
207
210
  // unlesbaren Antworten (3): der Nutzer hat hier nichts falsch gemacht.
211
+ const t = ARBEITSPLATZ_TEXTE[sprache()];
208
212
  if (dateien.length === 0) {
209
- console.error("Die Antwort enthielt keine einzige Datei -- das ist kein gueltiges Ergebnis.");
210
- console.error("Bitte gleich noch einmal versuchen.");
213
+ console.error(t.noFiles1);
214
+ console.error(t.noFiles2);
211
215
  return 3;
212
216
  }
213
217
  const ziel = resolve(ordnerName);
@@ -232,8 +236,8 @@ export function dateienSchreiben(dateien, ordnerName, hinweis) {
232
236
  nicht hineinwechseln. Der Rat war in diesem Zweig unausfuehrbar.
233
237
  *Ein Parameter, der zu einem Zweig passt, gehoert nicht in alle drei.*
234
238
  */
235
- console.error(`„${ordnerName}" gibt es schon -- als DATEI, nicht als Ordner.`);
236
- console.error("Benenn die Datei um oder geh in einen anderen Ordner.");
239
+ console.error(t.isAFile1(ordnerName));
240
+ console.error(t.isAFile2);
237
241
  return 1;
238
242
  }
239
243
  /*
@@ -247,22 +251,22 @@ export function dateienSchreiben(dateien, ordnerName, hinweis) {
247
251
  Bewusst eine geschlossene Liste und kein "alles, was mit Punkt anfaengt":
248
252
  Eine `.env` oder ein `.git` IST Inhalt, und den fassen wir nicht an.
249
253
  */
250
- const OS_KRIMSKRAMS = [".DS_Store", "desktop.ini", "Thumbs.db"];
254
+ const OS_KRIMSKRAMS = [".DS_Store", "desktop.ini", "Thumbs.db"]; // kein-anzeigetext: Dateinamen des Betriebssystems
251
255
  const echterInhalt = readdirSync(ziel).filter((n) => !OS_KRIMSKRAMS.includes(n));
252
256
  if (echterInhalt.length > 0) {
253
- console.error(`Der Ordner „${ordnerName}" gibt es schon und ist nicht leer.`);
254
- console.error("Ich fasse ihn nicht an -- so geht nie versehentlich eigene Arbeit verloren.");
257
+ console.error(t.notEmpty1(ordnerName));
258
+ console.error(t.notEmpty2);
255
259
  console.error(hinweis);
256
260
  return 1;
257
261
  }
258
262
  }
259
263
  }
260
264
  catch (fehler) {
261
- console.error(`Ich komme an den Ordner „${ordnerName}" nicht heran.`);
262
- console.error(`Grund: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
265
+ console.error(t.cannotReach1(ordnerName));
266
+ console.error(t.reason(fehler instanceof Error ? fehler.message : String(fehler)));
263
267
  // ⚠️ Ebenfalls OHNE den `hinweis` (Runde 2): Wer den Ordner nicht lesen darf,
264
268
  // kann auch nicht hineinwechseln. Hier hilft nur, was schon dasteht.
265
- console.error("Das liegt an diesem Rechner, nicht am Server -- Rechte pruefen.");
269
+ console.error(t.cannotReach2);
266
270
  return 1;
267
271
  }
268
272
  // 🔴 B-6: ALLE Pfade pruefen, BEVOR die erste Datei geschrieben wird. Wuerde
@@ -272,7 +276,7 @@ export function dateienSchreiben(dateien, ordnerName, hinweis) {
272
276
  // je erfaehrt, dass der Ordner unvollstaendig ist.
273
277
  for (const datei of dateien) {
274
278
  if (istUnsichererPfad(datei.pfad)) {
275
- console.error(`Die Antwort enthaelt einen unsicheren Pfad: „${datei.pfad}". Bitte melden.`);
279
+ console.error(t.unsafePath(datei.pfad));
276
280
  return 3;
277
281
  }
278
282
  }
@@ -304,9 +308,9 @@ export function dateienSchreiben(dateien, ordnerName, hinweis) {
304
308
  `readdirSync`-catch. *Ein Fehler, der einmal verstanden und
305
309
  aufgeschrieben wurde, ist im Nachbar-catch trotzdem wieder neu.*
306
310
  */
307
- console.error(`Schreiben fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
308
- console.error(`Der Ordner „${ordnerName}" ist dadurch UNVOLLSTAENDIG -- er darf geloescht werden.`);
309
- console.error("Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.");
311
+ console.error(t.writeFailed(fehler instanceof Error ? fehler.message : String(fehler)));
312
+ console.error(t.incomplete(ordnerName));
313
+ console.error(t.writeFailedBlame);
310
314
  return 1;
311
315
  }
312
316
  return 0;
package/dist/argumente.js CHANGED
@@ -6,6 +6,12 @@
6
6
  * Uebergehen. Wer `--titl` tippt, soll es erfahren, statt ein Projekt ohne
7
7
  * Titel anzulegen.
8
8
  */
9
+ import { SCHALTER_EN, deutscherSchalter } from "./befehlswoerter.js";
10
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
11
+ // `alsWort` wohnt seit Teil 3 in `wort.ts` (importfrei, die App liest es). Hier weiter
12
+ // erreichbar, damit kein Aufrufer im Programm umziehen muss.
13
+ import { alsWort } from "./wort.js";
14
+ export { alsWort };
9
15
  /**
10
16
  * Trennt die **vorderen Woerter** von den Optionen.
11
17
  *
@@ -65,98 +71,8 @@ export function alsBefehlszeile(optionen) {
65
71
  */
66
72
  return optionen.map((teil) => alsWort(teil)).join(" ");
67
73
  }
68
- /**
69
- * EIN Wort so in eine Befehlszeile setzen, dass es beim Kopieren wieder genau
70
- * dieses Wort ergibt.
71
- *
72
- * 🔴 **Gegen Befund 4 der zweiten Review-Runde (2026-08-27).** An NEUN Stellen
73
- * stand dieselbe Zeile — `wert.includes(" ") ? `"${wert}"` : wert` — und alle
74
- * neun hatten dieselbe Lücke: Ein Wert, der selbst ein Anführungszeichen
75
- * enthält, sprengt die Anführungszeichen, die man um ihn herum setzt.
76
- *
77
- * Der Fall: `kimal3 arbeitsplatz neu sabine --firma 'Blume "Nord" GmbH'`. Der
78
- * angebotene Rat lautete danach `--firma "Blume "Nord" GmbH"`. Wer ihn
79
- * kopiert, gibt seiner Kommandozeile **drei** Wörter statt einem und bekommt
80
- * eine andere, verwirrendere Absage als die, die er gerade gelesen hat.
81
- * *Ein Rat, der beim Kopieren zerfällt, ist ein Rat in eine zweite Absage* —
82
- * genau die Lehre, gegen die `alsBefehlszeile` oben gebaut wurde.
83
- *
84
- * 🔑 **Warum einfache Anführungszeichen und keine Maskierung mit `\`:**
85
- * `'…'` bedeutet in beiden Kommandozeilen dasselbe, die auf den Rechnern
86
- * unserer Kunden laufen — in der von Mac/Linux (`sh`) wie in der von Windows
87
- * (PowerShell). Eine Maskierung mit `\"` gilt nur in der ersten; in
88
- * PowerShell müsste man das Zeichen verdoppeln. *Eine Bauform, die nur auf
89
- * einem der beiden Rechner hält, ist in einem Produkt für beide keine
90
- * Bauform.*
91
- *
92
- * 🔑 **`immer` heisst „setz die Anfuehrungszeichen auch dann, wenn es ohne
93
- * ginge".** Die Zeilen unter „Deine Firmen:" haben sie seit jeher, und dort
94
- * gehoeren sie hin: Sie sind zum Kopieren UND zum Weitertippen gedacht -- wer
95
- * „Suedwind" durch „Blume & Co" ersetzt, hat die Anfuehrungszeichen dann schon
96
- * stehen. Beim Zusammenbauen einer getippten Zeile (`alsBefehlszeile`) und
97
- * beim `cd` waere das umgekehrt eine Aenderung an dem, was der Kunde bisher
98
- * sieht -- deshalb dort die Vorgabe `false`.
99
- *
100
- * ⚠️ **Die eine Lage, die offen bleibt:** ein Wert mit BEIDEN Sorten
101
- * Anführungszeichen. Dafür gibt es keine Schreibweise, die in beiden
102
- * Kommandozeilen dasselbe heißt. Der Rückfall maskiert nach `sh`-Regeln — und
103
- * für genau diesen Fall gibt es ohnehin `--firma-id`, das ohne jedes
104
- * Sonderzeichen auskommt.
105
- */
106
- export function alsWort(wert, immer = false) {
107
- if (wert === "")
108
- return '""';
109
- /*
110
- Was eine Kommandozeile nicht wörtlich stehen lässt: Leerzeichen (trennen),
111
- Anführungszeichen beider Sorten (zitieren) -- und `$` wie ````
112
- (rechnen).
113
-
114
- ⚠️ Der Rückwärtsstrich ist bewusst NICHT dabei: In einem Firmennamen
115
- kommt er nicht vor, und ihn mitzunehmen hieße, ihn auch maskieren zu
116
- müssen -- was in PowerShell wieder falsch wäre.
117
- */
118
- /*
119
- 🔴 **EINE ERLAUBNISLISTE, KEINE VERBOTSLISTE** (Befund 1 der FÜNFTEN
120
- Review-Runde, 2026-08-27) -- und die Verbotsliste hatte drei Runden
121
- gebraucht, um dreimal zu lecken: erst fehlten die Anführungszeichen, dann
122
- `$` und das Akzent-Zeichen, dann `&`, `;`, `|`, Klammern, `<`, `>`.
123
-
124
- Der Fall, der es aufgedeckt hat: eine Firma namens `Blume&Co` -- **ohne**
125
- Leerzeichen, also fiel sie durch jede Prüfung. Die angebotene Zeile hieß
126
- `--firma Blume&Co`; in PowerShell ist das ein Syntaxfehler, in `sh`
127
- startet es `--firma Blume` im Hintergrund und führt danach `Co` aus.
128
-
129
- 🔑 *Eine Liste dessen, was gefährlich ist, ist immer unvollständig -- die
130
- Liste dessen, was harmlos ist, kann man zu Ende schreiben.* Also
131
- umgekehrt: Buchstaben (auch Umlaute), Ziffern und eine Handvoll
132
- unverfänglicher Satzzeichen bleiben unangetastet, **alles andere wird
133
- zitiert**. Ein Anführungszeichen zu viel ist folgenlos; eines zu wenig
134
- hat jetzt dreimal einen Rat zerlegt.
135
- */
136
- if (!immer && /^[A-Za-z0-9À-ÿ._\/@:+-]+$/.test(wert))
137
- return wert;
138
- /*
139
- 🔴 **`$` UND ```` MÜSSEN AUS DEN DOPPELTEN ANFÜHRUNGSZEICHEN HERAUS**
140
- (Befund 4 der dritten Review-Runde, 2026-08-27). Doppelte Anführungszeichen
141
- halten Leerzeichen zusammen, aber sie schützen NICHT vor dem Rechnen: Eine
142
- Firma namens `Cash$$ GmbH` ergab die Zeile
143
- `--firma "Cash$$ GmbH"`, und `$$` ist in `sh` die Nummer des laufenden
144
- Programms. Der Kunde kopiert den angebotenen Befehl und liest danach
145
- *„Zu ‚Cash1234 GmbH' gehörst du nicht."* -- wieder ein Rat, der beim
146
- Kopieren zerfällt, nur eine Stufe subtiler als der Fall, gegen den diese
147
- Funktion gebaut wurde.
148
-
149
- Deshalb: doppelte Anführungszeichen nur, wenn wirklich nichts Rechnendes
150
- drinsteht. Sonst die einfachen, die in beiden Kommandozeilen alles wörtlich
151
- nehmen.
152
- */
153
- if (!/["$\`]/.test(wert))
154
- return `"${wert}"`;
155
- if (!wert.includes("'"))
156
- return `'${wert}'`;
157
- return `"${wert.replace(/(["$\`])/g, "\\$1")}"`;
158
- }
159
- export function zerlege(argv, erlaubt) {
74
+ export function zerlege(argv, erlaubt, sprache) {
75
+ const t = ALLGEMEIN_TEXTE[sprache];
160
76
  const optionen = {};
161
77
  for (let i = 0; i < argv.length; i++) {
162
78
  const teil = argv[i];
@@ -167,13 +83,17 @@ export function zerlege(argv, erlaubt) {
167
83
  if (!teil.startsWith("--")) {
168
84
  return {
169
85
  ok: false,
170
- meldung: `Mit „${teil}" kann ich hier nichts anfangen. Erwartet werden nur Optionen wie „--name wert".`,
86
+ meldung: t.cannotUseWord(teil),
171
87
  };
172
88
  }
173
89
  const gleich = teil.indexOf("=");
174
- const name = gleich === -1 ? teil.slice(2) : teil.slice(2, gleich);
90
+ // 🌐 Teil 3 (T3-4): `getippt` steht in jeder Meldung (der Mensch soll sein eigenes Wort
91
+ // wiedererkennen), `name` ist der deutsche Name, unter dem der Wert abgelegt wird und zum
92
+ // Server geht. Doppelt ist doppelt, egal in welcher Sprache (`--firma A --company B`, L-16).
93
+ const getippt = gleich === -1 ? teil.slice(2) : teil.slice(2, gleich);
94
+ const name = deutscherSchalter(getippt);
175
95
  if (!erlaubt.includes(name)) {
176
- return { ok: false, meldung: `Die Option „--${name}" kenne ich nicht.` };
96
+ return { ok: false, meldung: t.unknownOption(getippt) };
177
97
  }
178
98
  // ⚠️ **Dieselbe Option zweimal ist ein FEHLER** (QA-Befund B-3, 2026-08-18).
179
99
  //
@@ -198,7 +118,7 @@ export function zerlege(argv, erlaubt) {
198
118
  if (Object.hasOwn(optionen, name)) {
199
119
  return {
200
120
  ok: false,
201
- meldung: `Die Option „--${name}" steht zweimal da. Bitte nenne sie nur einmal.`,
121
+ meldung: t.duplicateOption(getippt),
202
122
  };
203
123
  }
204
124
  if (gleich !== -1) {
@@ -208,10 +128,18 @@ export function zerlege(argv, erlaubt) {
208
128
  const wert = argv[i + 1];
209
129
  // Eine Zahl mit Minus ist ein Wert; "--irgendwas" ist die naechste Option.
210
130
  if (wert === undefined || wert.startsWith("--")) {
211
- return { ok: false, meldung: `Der Option „--${name}" fehlt ihr Wert.` };
131
+ return { ok: false, meldung: t.missingValue(getippt) };
212
132
  }
213
133
  optionen[name] = wert;
214
134
  i++;
215
135
  }
216
136
  return { ok: true, werte: { optionen } };
217
137
  }
138
+ /**
139
+ * Ein Schalter OHNE Wert (`--ueberschreiben`, `--ohne-firma`) in beiden Sprachen: gesetzt? -- und
140
+ * `argv` ohne ihn, fuer `zerlege`, das nur Schalter MIT Wert kennt.
141
+ */
142
+ export function schalterOhneWert(argv, deutsch) {
143
+ const namen = [deutsch, ...Object.keys(SCHALTER_EN).filter((e) => SCHALTER_EN[e] === deutsch)].map((n) => `--${n}`);
144
+ return { gesetzt: argv.some((a) => namen.includes(a)), rest: argv.filter((a) => !namen.includes(a)) };
145
+ }