kimal3 0.3.2 → 0.4.0-durchstich.3

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.
@@ -18,6 +18,8 @@ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in C
18
18
  * Der Befehl, der in die Einstellungen eingetragen wird.
19
19
  *
20
20
  * 🔴 NUR EIN BEFEHL, keine Kommandozeilen-Zeichen -- kein `||`, kein `2>/dev/null`.
21
+ * 📌 Die eine Ausnahme seit dem 15.10.: Auf Mac und Linux steht `PATH="…" ` davor (`wacheBefehl` unten). Dort
22
+ * startet Claude Code Startbefehle immer über `sh -c`, und `sh` kennt diese Form; unter Windows bleibt er bloß.
21
23
  * Das sind Bash-Zeichen, und ob auf dem Kundenrechner eine Bash laeuft, wissen
22
24
  * wir nicht. Die Fehlerbehandlung gehoert ins Programm, nicht in die Zeile.
23
25
  * *Ein Befehl, der beim Erbauer laeuft, ist keine Aussage ueber den Rechner
@@ -38,6 +40,31 @@ export const WACHE_MARKE = "kimal3 wache"; // kein-anzeigetext: Startbefehl in C
38
40
  * AUCH BEI RUECKGABEWERT 127 gelesen (ebenfalls gemessen).
39
41
  */
40
42
  export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen, bleibt deutsch (T3-5)
43
+ /**
44
+ * Der Startbefehl, wie er auf DIESEM Rechner eingetragen wird. Auf Mac und Linux stehen die Ordner davor, in denen
45
+ * `kimal3` und Node.js liegen (`ordner`, doppelte nur einmal).
46
+ * 🔑 Warum beide: `kimal3` liegt dort, wo npm Programme ablegt -- beim Mac-Weg `~/.kimal3/node/bin`, bei Homebrew
47
+ * `/opt/homebrew/bin`; Node.js selbst liegt bei Homebrew im versionierten Ordner darunter. Ein `kimal3` in einem
48
+ * eigenen npm-Ordner braucht zusätzlich den Ordner von Node.js für seine erste Zeile (`#!/usr/bin/env node`).
49
+ *
50
+ * 🔑 WARUM: Startbefehle laufen mit Claudes eigener Umgebung über `sh -c` und lesen weder `~/.zprofile`
51
+ * noch `~/.zshrc` (code.claude.com/docs/en/hooks, gelesen 15.10.). Liegt Node.js im Benutzerordner
52
+ * (`~/.kimal3/node`, der Mac-Weg ohne Adminrechte) und wurde Claude aus dem Dock gestartet, fände `sh`
53
+ * weder `kimal3` noch das `node` aus dessen erster Zeile -- und die Wache schwiege, ohne dass es jemand merkt.
54
+ * Unter Windows bleibt es beim bloßen Befehl: Dort steht Node.js im Pfad des Systems, und die Wache läuft
55
+ * dort seit Wochen so.
56
+ * 🔴 Nur Zeichen aus einer kurzen Liste kommen in den Befehl (messen-und-pruefen §23); alles andere
57
+ * -- ein Anführungszeichen, ein `$` -- gäbe einen Befehl, den `sh` anders liest. Ein solcher Ordner fällt weg; bleibt
58
+ * keiner übrig, steht der bloße Befehl da.
59
+ */
60
+ export function wacheBefehl(system, ordner) {
61
+ // Ein Wegwerf-Ort fällt weg (Durchsicht 4, Fund 4): `npx kimal3 …` startet aus einem Zwischenspeicher, der bald
62
+ // verschwindet, ein Start aus einem Projektordner aus dessen `node_modules`.
63
+ const sicher = [...new Set(ordner)].filter((o) => /^\/[A-Za-z0-9 ._@+\/-]*$/.test(o) && !/\/(node_modules|_npx)\//.test(`${o}/`));
64
+ if (system === "win32" || sicher.length === 0)
65
+ return WACHE_BEFEHL;
66
+ return `PATH="${sicher.join(":")}:$PATH" ${WACHE_BEFEHL}`; // kein-anzeigetext: Startbefehl in Claudes Einstellungen
67
+ }
41
68
  /**
42
69
  * Wie lange Claude auf die Wache wartet, in Sekunden.
43
70
  *
@@ -48,9 +75,45 @@ export const WACHE_BEFEHL = `${WACHE_MARKE} --hook`; // kein-anzeigetext: Startb
48
75
  * seiner Arbeit ab -- und ausgerechnet unserer soll ihm helfen.*
49
76
  */
50
77
  export const WACHE_ZEITGRENZE = 10;
51
- function istObjekt(wert) {
78
+ export function istObjekt(wert) {
52
79
  return typeof wert === "object" && wert !== null && !Array.isArray(wert);
53
80
  }
81
+ /**
82
+ * Die Einstellungsdatei als Objekt -- oder die Absage. Gemeinsam fuer die Wache und die Plugins
83
+ * (`plugins-mischen.ts`), damit beide eine kaputte Datei gleich behandeln: nicht anfassen.
84
+ */
85
+ export function leseEinstellungen(alt) {
86
+ /*
87
+ Eine fehlende und eine leere Datei sind dasselbe: Es gibt nichts zu
88
+ bewahren, also legen wir an. `null` kommt vom Aufrufer, wenn die Datei gar
89
+ nicht existiert; ein leerer Text entsteht, wenn ein frueherer Lauf sie
90
+ angelegt, aber nicht mehr befuellt hat.
91
+ */
92
+ const leer = alt === null || alt.trim() === "";
93
+ if (leer)
94
+ return { ok: true, objekt: {}, leer };
95
+ let gelesen;
96
+ try {
97
+ // Eine unsichtbare Kennung am Anfang (UTF-8-BOM, z. B. von PowerShell 5.1 geschrieben) macht die
98
+ // Datei nicht kaputt -- `JSON.parse` stolpert aber darüber (QA-Fund 3 zum Bauplan 2026-10-15).
99
+ gelesen = JSON.parse(alt.replace(/^\uFEFF/, ""));
100
+ }
101
+ catch {
102
+ /*
103
+ 🔴 HIER WIRD NICHTS GESCHRIEBEN. Wer eine unlesbare Einstellungsdatei
104
+ ueberschreibt, wirft weg, was der Mensch dort stehen hat -- und
105
+ ausgerechnet der Fall, in dem sie schon beschaedigt ist, ist der, in
106
+ dem er sie am dringendsten braucht. Die Absage geht nach oben, der
107
+ Mensch erfaehrt sie.
108
+ */
109
+ return { ok: false, grund: "kein-gueltiges-json" };
110
+ }
111
+ // Eine Liste oder eine blosse Zahl ist gueltiges JSON, aber keine
112
+ // Einstellungsdatei. Das ist ein DRITTER Fall, nicht der zweite.
113
+ if (!istObjekt(gelesen))
114
+ return { ok: false, grund: "fremde-form" };
115
+ return { ok: true, objekt: gelesen, leer };
116
+ }
54
117
  /**
55
118
  * Ist das ein Eintrag, den WIR frueher einmal geschrieben haben?
56
119
  *
@@ -75,33 +138,10 @@ function istUnserer(eintrag) {
75
138
  return typeof eintrag.command === "string" && eintrag.command.includes(WACHE_MARKE);
76
139
  }
77
140
  export function mischeEinstellungen(alt, befehl) {
78
- /*
79
- Eine fehlende und eine leere Datei sind dasselbe: Es gibt nichts zu
80
- bewahren, also legen wir an. `null` kommt vom Aufrufer, wenn die Datei gar
81
- nicht existiert; ein leerer Text entsteht, wenn ein frueherer Lauf sie
82
- angelegt, aber nicht mehr befuellt hat.
83
- */
84
- const leer = alt === null || alt.trim() === "";
85
- let gelesen = {};
86
- if (!leer) {
87
- try {
88
- gelesen = JSON.parse(alt);
89
- }
90
- catch {
91
- /*
92
- 🔴 HIER WIRD NICHTS GESCHRIEBEN. Wer eine unlesbare Einstellungsdatei
93
- ueberschreibt, wirft weg, was der Mensch dort stehen hat -- und
94
- ausgerechnet der Fall, in dem sie schon beschaedigt ist, ist der, in
95
- dem er sie am dringendsten braucht. Die Absage geht nach oben, der
96
- Mensch erfaehrt sie.
97
- */
98
- return { art: "abgelehnt", grund: "kein-gueltiges-json" };
99
- }
100
- }
101
- // Eine Liste oder eine blosse Zahl ist gueltiges JSON, aber keine
102
- // Einstellungsdatei. Das ist ein DRITTER Fall, nicht der zweite.
103
- if (!istObjekt(gelesen))
104
- return { art: "abgelehnt", grund: "fremde-form" };
141
+ const lesen = leseEinstellungen(alt);
142
+ if (!lesen.ok)
143
+ return { art: "abgelehnt", grund: lesen.grund };
144
+ const { objekt: gelesen, leer } = lesen;
105
145
  const vorher = JSON.stringify(gelesen);
106
146
  const hooks = gelesen.hooks === undefined ? {} : gelesen.hooks;
107
147
  if (!istObjekt(hooks))
@@ -1,7 +1,8 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { WACHE_BEFEHL, mischeEinstellungen } from "./einstellungen-mischen.js";
2
+ import { homedir, platform } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { mischeEinstellungen, wacheBefehl } from "./einstellungen-mischen.js";
5
+ import { KATALOG, mischePlugins, pluginStand } from "./plugins-mischen.js";
5
6
  /** Die Kontrolle nach dem Schreiben fand unseren Eintrag nicht -- eigener Fall, damit `detail` `null` sein kann. */
6
7
  class EintragFehlt extends Error {
7
8
  }
@@ -12,6 +13,41 @@ export function sicherungsDatei() {
12
13
  return einstellungenDatei() + ".kimal3-sicherung";
13
14
  }
14
15
  export function trageWacheEin() {
16
+ // Vor den Befehl kommen der Ordner, aus dem `kimal3` gerade gestartet wurde (dort legt npm es ab; bei Homebrew liegt
17
+ // dort auch `node`), und der Ordner des laufenden Node.js (für ein `kimal3` in einem eigenen npm-Ordner) -- Durchsicht 3, Fund 2.
18
+ const befehl = wacheBefehl(platform(), [dirname(process.argv[1] ?? ""), dirname(process.execPath)]);
19
+ return trageEin((alt) => mischeEinstellungen(alt, befehl), (text) => text.includes(JSON.stringify(befehl)), sicherungsDatei());
20
+ }
21
+ /**
22
+ * Die Kopie, die die Plugins vor dem Schreiben anlegen. Eine eigene, weil `kimal3 einrichten` die Plugins
23
+ * VOR der Wache einträgt: So hält diese Kopie Sabines unveränderte Datei (Prüfer-Fund 9 zum Bauplan 2026-10-15).
24
+ */
25
+ export function pluginSicherungsDatei() {
26
+ return einstellungenDatei() + ".kimal3-sicherung-plugins";
27
+ }
28
+ /**
29
+ * superpowers und das Logo eintragen (`plugins-mischen.ts`); `katalogPfad` mit Schrägstrichen vorwärts,
30
+ * `null` ohne Logo-Ordner. 🔑 Die Kontrolle liest die Einschalter selbst, nicht nur Katalogname und Pfad:
31
+ * Fehlte ein Einschalter, meldete die Zeile `Plugins:` sonst „eingetragen“ für etwas, das nie lädt
32
+ * (Durchsicht 2, Fund 7). „Von Sabine ausgeschaltet“ zählt als da -- es steht ja drin.
33
+ */
34
+ export function tragePluginsEin(katalogPfad, mitSuperpowers) {
35
+ return trageEin((alt) => mischePlugins(alt, katalogPfad, mitSuperpowers), (text) => {
36
+ const stand = pluginStand(text);
37
+ if (mitSuperpowers && stand.superpowers === "fehlt")
38
+ return false;
39
+ return katalogPfad === null || (stand.logo !== "fehlt" && text.includes(`"${KATALOG}"`) && text.includes(JSON.stringify(katalogPfad)));
40
+ }, pluginSicherungsDatei());
41
+ }
42
+ /**
43
+ * Der Vier-Schritte-Weg fuer jeden Eintrag in Claudes Einstellungen -- die Wache und die Plugins
44
+ * (`plugins-mischen.ts`) gehen denselben, damit keiner von beiden ohne Sicherung schreibt.
45
+ * `kontrolle` bekommt den zurueckgelesenen Text und sagt, ob unser Eintrag drinsteht.
46
+ */
47
+ export function trageEin(mische, kontrolle,
48
+ // 🔑 Je Schreiber eine eigene Kopie: Schrieben Wache und Plugins in dieselbe, überschriebe die zweite
49
+ // Sabines Originaldatei mit dem Stand nach dem ersten Eintrag (QA-Fund 9).
50
+ sicherung) {
15
51
  const datei = einstellungenDatei();
16
52
  const gabEsSchon = existsSync(datei);
17
53
  // --- Schritt 1: lesen ---
@@ -30,15 +66,15 @@ export function trageWacheEin() {
30
66
  return { art: "gescheitert", schritt: "lesen", detail: e.message };
31
67
  }
32
68
  }
33
- const ergebnis = mischeEinstellungen(alt, WACHE_BEFEHL);
69
+ const ergebnis = mische(alt);
34
70
  if (ergebnis.art === "abgelehnt")
35
71
  return { art: "abgelehnt", grund: ergebnis.grund };
36
72
  if (ergebnis.art === "unveraendert")
37
- return { art: "stand-schon" };
73
+ return { art: "stand-schon", inhalt: alt ?? "" };
38
74
  // --- Schritt 2: Sicherungskopie ---
39
75
  if (gabEsSchon) {
40
76
  try {
41
- copyFileSync(datei, sicherungsDatei());
77
+ copyFileSync(datei, sicherung);
42
78
  }
43
79
  catch (e) {
44
80
  /*
@@ -55,22 +91,22 @@ export function trageWacheEin() {
55
91
  writeFileSync(datei, ergebnis.inhalt, "utf8");
56
92
  }
57
93
  catch (e) {
58
- zurueck(gabEsSchon);
94
+ zurueck(gabEsSchon, sicherung);
59
95
  return { art: "gescheitert", schritt: "schreiben", detail: e.message };
60
96
  }
61
97
  // --- Schritt 4: zuruecklesen und erneut pruefen ---
62
98
  try {
63
99
  const zurueckgelesen = readFileSync(datei, "utf8");
64
100
  const geparst = JSON.parse(zurueckgelesen);
65
- if (!zurueckgelesen.includes(WACHE_BEFEHL) || geparst === null) {
101
+ if (!kontrolle(zurueckgelesen) || geparst === null) {
66
102
  throw new EintragFehlt();
67
103
  }
68
104
  }
69
105
  catch (e) {
70
- zurueck(gabEsSchon);
106
+ zurueck(gabEsSchon, sicherung);
71
107
  return { art: "gescheitert", schritt: "kontrolle", detail: e instanceof EintragFehlt ? null : e.message };
72
108
  }
73
- return { art: "eingetragen" };
109
+ return { art: "eingetragen", inhalt: ergebnis.inhalt };
74
110
  }
75
111
  /**
76
112
  * Die Sicherung zurueckspielen.
@@ -79,11 +115,11 @@ export function trageWacheEin() {
79
115
  * bleibt die halb geschriebene stehen. Das ist die harmlose Seite: Sie
80
116
  * enthielt vorher nichts, was jemandem gehoerte.
81
117
  */
82
- function zurueck(gabEsSchon) {
118
+ function zurueck(gabEsSchon, sicherung) {
83
119
  if (!gabEsSchon)
84
120
  return;
85
121
  try {
86
- copyFileSync(sicherungsDatei(), einstellungenDatei());
122
+ copyFileSync(sicherung, einstellungenDatei());
87
123
  }
88
124
  catch {
89
125
  /* Mehr koennen wir nicht tun -- der Grund steht in der Meldung oben. */
package/dist/kimal3.js CHANGED
@@ -20,11 +20,12 @@ import { WACHE_TEXTE } from "./texte/wache.js";
20
20
  import { leseWacheDatei, sitzungJetzt, wacheAusgefallen } from "./wache-schalter.js";
21
21
  import { formatiere, heuteAus, skillZustand, sollZeigen, } from "./nachricht.js";
22
22
  import { eigeneVersion } from "./version.js";
23
- import { sprache } from "./sprache.js";
23
+ import { merkeBefehlsSprache, sprache } from "./sprache.js";
24
24
  import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
25
25
  import { hilfeText } from "./hilfe.js";
26
- import { erkenneBefehl } from "./befehlswoerter.js";
26
+ import { BEFEHLSWOERTER, erkenneBefehl } from "./befehlswoerter.js";
27
27
  import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
28
+ import { einrichten } from "./befehle/einrichten.js";
28
29
  import { werkbankNeu } from "./befehle/werkbank.js";
29
30
  import { firmaDaten } from "./befehle/firma-daten.js";
30
31
  import { kontextFragen } from "./befehle/kontext-fragen.js";
@@ -484,6 +485,7 @@ async function fuehreAus(name, rest) {
484
485
  case "projekt liste": return allein ? projektListe() : null;
485
486
  // ⚠️ Rest wird durchgereicht, nicht auf Gleichheit geprueft: `firma daten --firma X` gehoert dazu.
486
487
  case "firma daten": return firmaDaten(rest);
488
+ case "einrichten": return einrichten(rest);
487
489
  case "arbeitsplatz neu": return arbeitsplatzNeu(rest);
488
490
  case "werkbank neu": return werkbankNeu(rest);
489
491
  case "kontext runter": return kontextRunter(rest);
@@ -493,7 +495,8 @@ async function fuehreAus(name, rest) {
493
495
  case "wache aus": return allein ? wacheSchalten(false) : null;
494
496
  case "wache":
495
497
  // 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install` eintraegt (T3-5:
496
- // eingetragen wird immer `kimal3 wache --hook`; `kimal3 watch --hook` laeuft genauso).
498
+ // eingetragen wird immer `kimal3 wache --hook`, auf Mac/Linux mit `PATH="…"` davor -- `wacheBefehl`;
499
+ // `kimal3 watch --hook` laeuft genauso).
497
500
  // Er verpackt die Meldung anders -- Begruendung an `wache()`.
498
501
  // ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
499
502
  if (rest.length === 1 && rest[0] === "--hook")
@@ -537,6 +540,10 @@ async function haupt() {
537
540
  // extra` bleibt unbekannt. Der grosse Kommentar zur Nachsicht bei `wache --irgendwas` steht
538
541
  // unten an `fuehreAus` und gilt unveraendert.
539
542
  const erkannt = erkenneBefehl(argv);
543
+ // 🌐 `setup` spricht Englisch, `einrichten` Deutsch, solange keine Kontosprache gemerkt ist --
544
+ // die Anleitung, die den Befehl tippt, liest seine Marken (Begruendung an `startSprache`).
545
+ if (erkannt?.name === "einrichten")
546
+ merkeBefehlsSprache(argv[0] === BEFEHLSWOERTER.einrichten.en ? "en" : "de");
540
547
  if (erkannt) {
541
548
  const code = await fuehreAus(erkannt.name, erkannt.rest);
542
549
  if (code !== null)
@@ -0,0 +1,117 @@
1
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ /**
6
+ * Legt den Plugin-Katalog mit dem Logo-Mod in `~/.kimal3/claude-plugins` ab und setzt dort die
7
+ * Sprache des Kontos. Claude Code liest den Mod aus genau diesem Ordner (Katalog `kimal3`,
8
+ * `plugins-mischen.ts`).
9
+ *
10
+ * 🔑 Warum ein fester Ordner im Benutzerordner und nicht der Ordner im installierten npm-Paket:
11
+ * Der hängt am Installationsort von Node.js und wechselt mit ihm -- der Eintrag in Claudes
12
+ * Einstellungen zeigte dann ins Leere.
13
+ * 🔑 Der Ordner gehört `kimal3` (seine README sagt es): Jeder Lauf ersetzt ihn ganz (Lage P-11).
14
+ */
15
+ export function katalogOrdner() {
16
+ return join(homedir(), ".kimal3", "claude-plugins"); // kein-anzeigetext: Ordnername
17
+ }
18
+ /** Die Zeile im Mod, die die Sprache festlegt -- `check:logo` prüft, dass es sie genau einmal gibt. */
19
+ export const SPRACH_ZEILE = "const SPRACHE: 'de' | 'en' = 'de'"; // kein-anzeigetext: Quelltext des Mods
20
+ /** Reine Rechnung: der Mod-Text in der Sprache des Kontos, oder `null`, wenn die Zeile fehlt. */
21
+ export function mitSprache(quelltext, sprache) {
22
+ if (quelltext.split(SPRACH_ZEILE).length !== 2)
23
+ return null;
24
+ return quelltext.replace(SPRACH_ZEILE, `const SPRACHE: 'de' | 'en' = '${sprache}'`); // kein-anzeigetext: Quelltext des Mods
25
+ }
26
+ /**
27
+ * 🔴 NIE EIN HALBER ORDNER (Durchsicht-Funde 2 und 9): Claudes Einstellungen zeigen auf `katalogOrdner()`,
28
+ * und eine laufende Claude-Sitzung hat den Mod daraus geladen. Deshalb:
29
+ * 1. Sprache setzen, BEVOR irgendetwas angefasst wird -- fehlt die Zeile, bleibt alles, wie es war.
30
+ * 2. Steht schon genau das da, was entstehen soll: nichts anfassen (Lage P-8). Das ist auch der Normalfall
31
+ * eines zweiten Laufs, und so kommt er gar nicht erst in die Nähe der Windows-Sperren unten.
32
+ * 3. Den neuen Ordner daneben fertig bauen (`….neu`), dann tauschen: alten Ordner wegschieben, neuen an
33
+ * seinen Platz, alten löschen.
34
+ * 🔑 Der weggeschobene Ordner bekommt bei JEDEM Lauf einen frischen Namen (Durchsicht 2, Funde 2 und 4):
35
+ * Ein Rest, den Windows festhält, blockiert so keinen späteren Lauf, und zurückgeholt wird nur, was
36
+ * DIESER Lauf weggeschoben hat -- nie ein veralteter Rest von früher.
37
+ * Lässt sich der alte nicht wegschieben (Claude hält ihn manchmal fest), bleibt er unversehrt stehen und
38
+ * gilt weiter: ein Erfolg mit `alterStand`, den `kimal3 einrichten` in einer Zeile nennt.
39
+ */
40
+ export function logoAblegen(sprache) {
41
+ // `dist/logo-ablegen.js` -> `../claude-plugins` im installierten Paket (`files` in package.json).
42
+ const quelle = fileURLToPath(new URL("../claude-plugins", import.meta.url));
43
+ const ziel = katalogOrdner();
44
+ const neu = `${ziel}.neu`; // kein-anzeigetext: Ordnername
45
+ const modDatei = (ordner) => join(ordner, "kimal3-logo", "hooks", "register.tsx");
46
+ try {
47
+ const text = mitSprache(readFileSync(modDatei(quelle), "utf8"), sprache);
48
+ if (text === null)
49
+ return { ok: false, detail: modDatei(quelle) };
50
+ if (stehtSchonDa(quelle, ziel, text)) {
51
+ // Auch im Normalfall aufräumen, samt einem halb gebauten `….neu`: sonst blieben Reste für immer (Durchsicht 3/4).
52
+ rmSync(neu, { recursive: true, force: true });
53
+ resteWegraeumen(ziel);
54
+ return { ok: true, ordner: ziel };
55
+ }
56
+ rmSync(neu, { recursive: true, force: true });
57
+ cpSync(quelle, neu, { recursive: true });
58
+ writeFileSync(modDatei(neu), text, "utf8");
59
+ if (!existsSync(ziel)) {
60
+ renameSync(neu, ziel);
61
+ return { ok: true, ordner: ziel };
62
+ }
63
+ const alt = `${ziel}.alt-${Date.now()}`; // kein-anzeigetext: Ordnername
64
+ try {
65
+ renameSync(ziel, alt);
66
+ }
67
+ catch {
68
+ rmSync(neu, { recursive: true, force: true });
69
+ return { ok: true, ordner: ziel, alterStand: true };
70
+ }
71
+ try {
72
+ renameSync(neu, ziel);
73
+ }
74
+ catch (e) {
75
+ renameSync(alt, ziel);
76
+ return { ok: false, detail: e.message };
77
+ }
78
+ resteWegraeumen(ziel);
79
+ return { ok: true, ordner: ziel };
80
+ }
81
+ catch (e) {
82
+ return { ok: false, detail: e.message };
83
+ }
84
+ }
85
+ /** Gleiche Dateien, gleicher Inhalt -- der Mod mit der Sprache des Kontos, alles andere wie im Paket. */
86
+ function stehtSchonDa(quelle, ziel, modText) {
87
+ if (!existsSync(ziel))
88
+ return false;
89
+ const dateien = (ordner) => alleDateien(ordner).sort();
90
+ const soll = dateien(quelle);
91
+ if (JSON.stringify(soll) !== JSON.stringify(dateien(ziel)))
92
+ return false;
93
+ const mod = join("kimal3-logo", "hooks", "register.tsx");
94
+ return soll.every((d) => (d === mod ? Buffer.from(modText, "utf8") : readFileSync(join(quelle, d))).equals(readFileSync(join(ziel, d))));
95
+ }
96
+ /**
97
+ * Alle Dateien unter `ordner`, als Pfade relativ dazu. 🔑 Von Hand statt `readdirSync(…, { recursive: true })`: Das kann
98
+ * Node.js erst ab 20.1, und unter 20.0 fände der Vergleich gar keine Datei -- „gleich“, und das Logo würde nie erneuert
99
+ * (Durchsicht 3, Fund 7; `engines` erlaubt 20).
100
+ */
101
+ function alleDateien(ordner, unter = "") {
102
+ return readdirSync(join(ordner, unter), { withFileTypes: true }).flatMap((e) => e.isDirectory() ? alleDateien(ordner, join(unter, e.name)) : e.isFile() ? [join(unter, e.name)] : []);
103
+ }
104
+ /** Weggeschobene Ordner früherer Läufe (`claude-plugins.alt…`); was Windows noch festhält, bleibt bis zum nächsten Lauf. */
105
+ function resteWegraeumen(ziel) {
106
+ const name = `${basename(ziel)}.alt`; // kein-anzeigetext: Ordnername
107
+ for (const eintrag of readdirSync(dirname(ziel))) {
108
+ if (!eintrag.startsWith(name))
109
+ continue;
110
+ try {
111
+ rmSync(join(dirname(ziel), eintrag), { recursive: true, force: true });
112
+ }
113
+ catch {
114
+ /* festgehalten -- der nächste Lauf versucht es wieder */
115
+ }
116
+ }
117
+ }
package/dist/nachricht.js CHANGED
@@ -259,10 +259,14 @@ export function stempelZeile(version, sprache) {
259
259
  }
260
260
  /**
261
261
  * Die Sprache des LETZTEN Stempels (wie `stempelAus`: ein Beispiel im Text darf nicht gewinnen).
262
+ *
263
+ * 🔴 Die Vorabfassung (`0.4.0-durchstich.1`) trägt einen Zusatz hinter den Zahlen -- ohne `(?:-…)?`
264
+ * fand das Muster bei ihr die Sprache nie, jeder englische Stempel galt als deutsch, und der Login
265
+ * stellte nichts mehr um (gemessen von `check:cli-live` L1/L6 beim Task 7 des Durchstichs).
262
266
  * Ohne Zusatz -- und bei jedem anderen Zusatz als genau `en` -- Deutsch (T4-4).
263
267
  */
264
268
  export function stempelSpracheAus(inhalt) {
265
- const treffer = [...inhalt.matchAll(/kimal3:erzeugt version=[0-9][0-9.]*(?: sprache=([a-z]+))?/g)];
269
+ const treffer = [...inhalt.matchAll(/kimal3:erzeugt version=[0-9][0-9.]*(?:-[0-9A-Za-z.]+)?(?: sprache=([a-z]+))?/g)];
266
270
  return treffer.length > 0 && treffer[treffer.length - 1][1] === "en" ? "en" : "de";
267
271
  }
268
272
  /**
@@ -0,0 +1,78 @@
1
+ import { istObjekt, leseEinstellungen } from "./einstellungen-mischen.js";
2
+ /**
3
+ * Traegt superpowers und das KImal3-Logo in Claudes Einstellungen ein -- als Katalog plus
4
+ * Einschalter, so wie Anthropic Firmen-Mods auf jeden Rechner verteilt (Bauplan 2026-10-15,
5
+ * „Warum anders als die Spec"). Beim naechsten Start laedt Claude Code beides selbst.
6
+ *
7
+ * 🔴 DIESE DATEI FASST KEINE DATEI AN -- dieselbe Bauform wie `einstellungen-mischen.ts`, aus
8
+ * demselben Grund: In `~/.claude/settings.json` steht der Arbeitsplatz des Menschen.
9
+ * Geschrieben wird ueber `einstellungen-schreiben.ts` (Sicherung, Zuruecklesen).
10
+ */
11
+ export const KATALOG = "kimal3"; // kein-anzeigetext: Name des Plugin-Katalogs in Claudes Einstellungen
12
+ export const LOGO = `kimal3-logo@${KATALOG}`; // kein-anzeigetext: Plugin-Kennung
13
+ const OFFIZIELL = "claude-plugins-official"; // kein-anzeigetext: Anthropics Katalog
14
+ export const SUPERPOWERS = `superpowers@${OFFIZIELL}`; // kein-anzeigetext: Plugin-Kennung
15
+ /**
16
+ * 🔑 Jede Entscheidung des Menschen bleibt stehen (Lagen P-5 bis P-7, P-10):
17
+ * - Ein Eintrag fuer das Logo -- auch `false` -- heisst: Er hat schon entschieden. Wir setzen
18
+ * nur, was fehlt.
19
+ * - superpowers aus IRGENDEINEM Katalog (`superpowers@…`, an oder aus) heisst: nicht noch eins.
20
+ * Zwei superpowers gaeben jede Faehigkeit doppelt.
21
+ * - Den offiziellen Katalog tragen wir nur ein, wenn er fehlt und wir superpowers einschalten.
22
+ * Nur der Katalog `kimal3` gehoert uns: Er zeigt immer auf unseren Ordner (P-9, Umzug).
23
+ * 🔑 `katalogPfad` ist `null`, wenn der Logo-Ordner nicht angelegt werden konnte: Dann bleiben Katalog und
24
+ * Logo-Eintrag, wie sie sind, und superpowers kommt trotzdem -- es braucht den Ordner nicht (Durchsicht 2, Fund 1).
25
+ * 🔑 `mitSuperpowers` ist `false`, wenn Git fehlt: Claude Code klont Anthropics Katalog mit Git und scheiterte sonst bei
26
+ * jedem Start (Durchsicht 3, Fund 5). Was schon drinsteht, bleibt -- auch ein früher eingetragenes superpowers.
27
+ */
28
+ export function mischePlugins(alt, katalogPfad, mitSuperpowers = true) {
29
+ const lesen = leseEinstellungen(alt);
30
+ if (!lesen.ok)
31
+ return { art: "abgelehnt", grund: lesen.grund };
32
+ const { objekt, leer } = lesen;
33
+ const kataloge = objekt.extraKnownMarketplaces ?? {};
34
+ const plugins = objekt.enabledPlugins ?? {};
35
+ if (!istObjekt(kataloge) || !istObjekt(plugins))
36
+ return { art: "abgelehnt", grund: "fremde-form" };
37
+ // Nur die Quelle gehört uns; was Claude Code selbst in den Eintrag schreibt (`autoUpdate`, wenn
38
+ // Sabine es unter /plugin umschaltet), bleibt stehen (QA-Fund 7). Zeigt die Quelle schon auf unseren
39
+ // Ordner, bleibt der Eintrag Zeichen für Zeichen, wie er ist -- auch in anderer Reihenfolge der
40
+ // Schlüssel; sonst schriebe jeder Lauf die Datei neu (Durchsicht-Fund 6, Lage P-8).
41
+ const alterEintrag = istObjekt(kataloge[KATALOG]) ? kataloge[KATALOG] : {};
42
+ const alteQuelle = istObjekt(alterEintrag.source) ? alterEintrag.source : {};
43
+ const zeigtSchonHin = alteQuelle.source === "directory" && alteQuelle.path === katalogPfad;
44
+ const neueKataloge = { ...kataloge };
45
+ const neuePlugins = { ...plugins };
46
+ if (katalogPfad !== null) {
47
+ neueKataloge[KATALOG] = zeigtSchonHin ? alterEintrag : { ...alterEintrag, source: { source: "directory", path: katalogPfad } };
48
+ if (!(LOGO in plugins))
49
+ neuePlugins[LOGO] = true;
50
+ }
51
+ if (mitSuperpowers && !Object.keys(plugins).some((k) => k.startsWith("superpowers@"))) {
52
+ neuePlugins[SUPERPOWERS] = true;
53
+ if (!(OFFIZIELL in kataloge)) {
54
+ neueKataloge[OFFIZIELL] = { source: { source: "github", repo: "anthropics/claude-plugins-official" } };
55
+ }
56
+ }
57
+ const neu = { ...objekt, extraKnownMarketplaces: neueKataloge, enabledPlugins: neuePlugins };
58
+ // Wie bei der Wache: Wer nichts zu tun hat, fasst die Datei nicht an (P-8).
59
+ if (!leer && JSON.stringify(neu) === JSON.stringify(objekt))
60
+ return { art: "unveraendert" };
61
+ return { art: "geschrieben", inhalt: JSON.stringify(neu, null, 2) + "\n" };
62
+ }
63
+ /**
64
+ * Was nach dem Eintragen wirklich gilt -- fuer die Zeile `Plugins:`, die Claude der Kundin weitersagt.
65
+ * 🔑 Aus der Datei gelesen, nicht aus dem, was wir eingetragen haben: Hat sie superpowers selbst
66
+ * ausgeschaltet, muss die Zeile das sagen, damit die Anleitung sie nicht ins Menue schickt (Durchsicht-Fund 1).
67
+ */
68
+ export function pluginStand(inhalt) {
69
+ const lesen = leseEinstellungen(inhalt);
70
+ const plugins = lesen.ok ? lesen.objekt.enabledPlugins : undefined;
71
+ if (!istObjekt(plugins))
72
+ return { superpowers: "fehlt", logo: "fehlt" };
73
+ const superpowers = Object.entries(plugins).filter(([k]) => k.startsWith("superpowers@")).map(([, v]) => v);
74
+ return {
75
+ superpowers: superpowers.includes(true) ? "an" : superpowers.length > 0 ? "aus" : "fehlt",
76
+ logo: plugins[LOGO] === true ? "an" : LOGO in plugins ? "aus" : "fehlt",
77
+ };
78
+ }
package/dist/sprache.js CHANGED
@@ -40,11 +40,21 @@ export function spracheAusSystem(gebiet) {
40
40
  return klein === "en" || klein.startsWith("en-") ? "en" : "de";
41
41
  }
42
42
  /**
43
- * Die Sprache beim Start (T3-2): Merkzettel -> sonst Deutsch, wenn schon eine Anmeldung liegt
44
- * (wer von einer aelteren Fassung aktualisiert: Sabine mit englischem Windows sah bisher immer
45
- * Deutsch) -> sonst das Betriebssystem.
43
+ * Die Sprache beim Start (T3-2): die Sprache des getippten Befehlsworts, wo es eine traegt ->
44
+ * sonst der Merkzettel -> sonst Deutsch, wenn schon eine Anmeldung liegt (wer von einer aelteren
45
+ * Fassung aktualisiert: Sabine mit englischem Windows sah bisher immer Deutsch) -> sonst das
46
+ * Betriebssystem.
47
+ *
48
+ * 🔑 DAS BEFEHLSWORT GANZ VORN (Durchstich, Gesamtpruefung Punkt 3 und Review a73305f, Befund 1):
49
+ * `kimal3 setup` tippt nur die englische Anleitung, und die wartet auf `Sign-in code:`. Jim mit
50
+ * englischem Konto und deutschem Windows bekam sonst `Anmelde-Code:`. Auch VOR dem Merkzettel:
51
+ * Der uebersteht `logout` -- meldet Jim auf Kemals Laptop erst Kemal ab, laege dort Kemals
52
+ * Deutsch. Gilt nur bis zur ersten Antwort des Servers; ab da die Kontosprache
53
+ * (`uebernimmKontosprache`).
46
54
  */
47
55
  export function startSprache(lage) {
56
+ if (lage.befehlsSprache !== null)
57
+ return lage.befehlsSprache;
48
58
  const gemerkt = alsSprache(lage.merkzettel);
49
59
  if (gemerkt !== null)
50
60
  return gemerkt;
@@ -78,12 +88,24 @@ function leseMerkzettel() {
78
88
  }
79
89
  let gemerkt;
80
90
  let aktuell = null;
91
+ let befehlsSprache = null;
92
+ /**
93
+ * Der Verteiler meldet hier die Sprache des getippten Befehlsworts -- nur fuer `einrichten`/`setup`,
94
+ * den einen Befehl, den eine Anleitung auf einem frischen Rechner aufruft und dessen Ausgabe sie
95
+ * liest. Er laeuft vor der ersten Ausgabe; `aktuell` wird trotzdem verworfen, damit die Reihenfolge
96
+ * nicht stillschweigend daran haengt.
97
+ */
98
+ export function merkeBefehlsSprache(s) {
99
+ befehlsSprache = s;
100
+ aktuell = null;
101
+ }
81
102
  /** Die Sprache dieses Laufs. */
82
103
  export function sprache() {
83
104
  if (aktuell === null) {
84
105
  gemerkt = leseMerkzettel();
85
106
  aktuell = startSprache({
86
107
  merkzettel: gemerkt,
108
+ befehlsSprache,
87
109
  angemeldet: leseAnmeldung() !== null,
88
110
  systemGebiet: Intl.DateTimeFormat().resolvedOptions().locale,
89
111
  });
@@ -97,10 +119,12 @@ export function sprache() {
97
119
  */
98
120
  export function uebernimmKontosprache(kopf) {
99
121
  sprache();
122
+ // 🔑 `aktuell` folgt dem Kopf auch dann, wenn der Merkzettel schon dieselbe Sprache traegt --
123
+ // sonst bliebe ein Lauf, der mit dem Befehlswort begann (`setup` bei deutschem Konto), englisch.
124
+ aktuell = alsSprache(kopf) ?? aktuell;
100
125
  const neu = nachAntwort(gemerkt, kopf);
101
126
  if (neu === null)
102
127
  return;
103
- aktuell = neu;
104
128
  gemerkt = neu;
105
129
  try {
106
130
  // ⚠️ `mode: 0o700` wie in anmeldung.ts und gesehen.ts -- wer den Ordner zuerst anlegt, legt
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Die Zeilenanfaenge, an denen Claude die Ausgabe von `kimal3 einrichten` liest
3
+ * (Anleitung `/einrichten` und `/setup`, Schritt 3). Eine Stelle fuer beide: Das
4
+ * Programm druckt sie, die Anleitung nennt sie, `check:kurs` haelt beide gleich.
5
+ */
6
+ export const EINRICHTEN_MARKEN = {
7
+ de: { code: "Anmelde-Code:", adresse: "Adresse:", arbeitsplatz: "Arbeitsplatz:", werkbank: "Werkbank:", angemeldet: "Angemeldet als", plugins: "Plugins:" },
8
+ en: { code: "Sign-in code:", adresse: "Address:", arbeitsplatz: "Workplace:", werkbank: "Workbench:", angemeldet: "Signed in as", plugins: "Plugins:" },
9
+ };
@@ -0,0 +1,60 @@
1
+ import { EINRICHTEN_MARKEN } from "./einrichten-marken.js";
2
+ const M = EINRICHTEN_MARKEN;
3
+ const de = {
4
+ codeLine: (code) => `\n ${M.de.code} ${code}`,
5
+ addressLine: (url) => ` ${M.de.adresse} ${url}`,
6
+ browserOpened: " Ich öffne die Seite gerade in deinem Browser. Bestätige dort nur, wenn derselbe Code dasteht.",
7
+ browserNotOpened: " Öffne die Adresse oben in deinem Browser. Bestätige dort nur, wenn derselbe Code dasteht.",
8
+ help: "\n kimal3 einrichten [--firma \"Name\" | --firma-id \"…\"]\n\n Meldet dich an (der Browser öffnet sich mit deinem Code), legt deinen\n KI-Arbeitsplatz in deinem Benutzerordner an, richtet die /k3-Befehle und\n die Wache ein und hakt die Schritte im Kurs ab. Ein zweiter Lauf ergänzt\n nur, was fehlt.\n",
9
+ start: "\nIch richte deinen KI-Arbeitsplatz ein.",
10
+ filesWritten: (neu, schonDa) => `\n✓ ${neu} Dateien angelegt, ${schonDa} lagen schon da.`,
11
+ workplaceLine: (pfad) => ` ${M.de.arbeitsplatz} ${pfad}`,
12
+ workbenchLine: (name) => ` ${M.de.werkbank} ${name}`,
13
+ courseMarked: "✓ Im Kurs abgehakt.",
14
+ courseNotMarked: " Die Haken im Kurs konnte ich gerade nicht setzen — der nächste Lauf holt das nach.",
15
+ done: "\n✓ Fertig eingerichtet.",
16
+ unsafeFolder: (name) => `Der Server hat einen Ordnernamen geschickt, den ich nicht anlege: ${name}. Bitte melde das an kimal3.`,
17
+ noWorkbank: "In der Lieferung steckt keine Werkbank. Bitte melde das an kimal3.",
18
+ // Plugins (Bauplan 2026-10-15): Claude liest die Zeile und sagt der Kundin, was nach dem Neustart erscheint.
19
+ logoName: "KImal3-Logo",
20
+ // `an`: eingeschaltet · `aus`: von ihr selbst ausgeschaltet (bleibt so, die Anleitung schickt sie dann nicht ins Menü)
21
+ // · `neu`: gerade eingetragen (kommt mit dem Neustart) oder stand schon da (Durchsicht-Funde 1 und 7).
22
+ pluginsLine: (an, aus, neu) => ` ${M.de.plugins} ${an.length > 0 ? `${an.join(", ")} — ${neu ? "erscheinen, sobald Claude ganz beendet und neu geöffnet ist" : "sind schon eingetragen"}.` : "nichts eingeschaltet."}` +
23
+ (aus.length > 0 ? ` Selbst ausgeschaltet, bleibt aus: ${aus.join(", ")}.` : ""),
24
+ pluginsNotWritten: (grund) => ` ${M.de.plugins} nicht eingetragen — ${grund}`,
25
+ settingsInvalid: "deine Claude-Einstellungen (~/.claude/settings.json) sind kein gültiges JSON. Ich habe sie nicht angefasst.",
26
+ settingsForeign: "in deinen Claude-Einstellungen (~/.claude/settings.json) steht etwas in einer Form, die ich nicht kenne. Ich habe sie nicht angefasst.",
27
+ settingsFailed: (ursache) => `deine Claude-Einstellungen (~/.claude/settings.json): ${ursache}.`,
28
+ // Logo (Durchsicht 2, Funde 1 und 3): Scheitert der Ordner, kommt superpowers trotzdem; hält Claude ihn fest, gilt die alte Fassung.
29
+ logoNotWritten: (detail) => ` Den Ordner für das KImal3-Logo konnte ich nicht anlegen oder erneuern (${detail}).`,
30
+ // Ohne Git kein superpowers (Durchsicht 3, Fund 5); die Anleitung zitiert den Anfang dieser Zeile.
31
+ superpowersNeedsGit: " superpowers braucht Git — es kommt mit dem nächsten `kimal3 einrichten`, sobald Git da ist.",
32
+ logoOldVersion: " Das KImal3-Logo bleibt vorerst in der bisherigen Fassung, weil Claude den Ordner gerade benutzt. Beende Claude ganz und starte danach `kimal3 einrichten` noch einmal.",
33
+ };
34
+ const en = {
35
+ codeLine: (code) => `\n ${M.en.code} ${code}`,
36
+ addressLine: (url) => ` ${M.en.adresse} ${url}`,
37
+ browserOpened: " Opening the page in your browser now. Only confirm there if it shows the same code.",
38
+ browserNotOpened: " Open the address above in your browser. Only confirm there if it shows the same code.",
39
+ help: "\n kimal3 setup [--company \"Name\" | --company-id \"…\"]\n\n Signs you in (your browser opens with your code), creates your AI Workplace\n in your user folder, sets up the /k3 commands and the watch, and ticks off\n the course steps. A second run only adds what is missing.\n",
40
+ start: "\nSetting up your AI Workplace.",
41
+ filesWritten: (neu, schonDa) => `\n✓ ${neu} files created, ${schonDa} were already there.`,
42
+ workplaceLine: (pfad) => ` ${M.en.arbeitsplatz} ${pfad}`,
43
+ workbenchLine: (name) => ` ${M.en.werkbank} ${name}`,
44
+ courseMarked: "✓ Ticked off in the course.",
45
+ courseNotMarked: " I couldn't tick off the course steps just now — the next run will catch up.",
46
+ done: "\n✓ All set up.",
47
+ unsafeFolder: (name) => `The server sent a folder name I won't create: ${name}. Please report this to kimal3.`,
48
+ noWorkbank: "The delivery contains no workbench. Please report this to kimal3.",
49
+ logoName: "KImal3 logo",
50
+ pluginsLine: (an, aus, neu) => ` ${M.en.plugins} ${an.length > 0 ? `${an.join(", ")} — ${neu ? "will appear once Claude is fully quit and reopened" : "already added"}.` : "nothing switched on."}` +
51
+ (aus.length > 0 ? ` Switched off by you, stays off: ${aus.join(", ")}.` : ""),
52
+ pluginsNotWritten: (grund) => ` ${M.en.plugins} not added — ${grund}`,
53
+ settingsInvalid: "your Claude settings (~/.claude/settings.json) are not valid JSON. I left them untouched.",
54
+ settingsForeign: "your Claude settings (~/.claude/settings.json) contain something in a form I don't know. I left them untouched.",
55
+ settingsFailed: (ursache) => `your Claude settings (~/.claude/settings.json): ${ursache}.`,
56
+ logoNotWritten: (detail) => ` I couldn't create or renew the folder for the KImal3 logo (${detail}).`,
57
+ superpowersNeedsGit: " superpowers needs Git — it comes with the next `kimal3 setup` once Git is there.",
58
+ logoOldVersion: " The KImal3 logo stays on its previous version for now, because Claude is using the folder. Quit Claude completely, then run `kimal3 setup` again.",
59
+ };
60
+ export const EINRICHTEN_TEXTE = { de, en };