kimal3 0.6.4 β 0.7.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.
- package/README.md +10 -4
- package/claude-plugins/.claude-plugin/marketplace.json +2 -1
- package/claude-plugins/README.md +2 -0
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +1 -1
- package/claude-plugins/kimal3-logo/hooks/ablage.ts +8 -0
- package/claude-plugins/kimal3-logo/hooks/app-oeffnen.ts +37 -9
- package/claude-plugins/kimal3-logo/hooks/register.tsx +42 -18
- package/claude-plugins/kimal3-logo/hooks/update-rechnung.ts +175 -0
- package/claude-plugins/kimal3-logo/hooks/update.ts +244 -0
- package/claude-plugins/kimal3-logo/types/index.d.ts +4 -0
- package/claude-plugins/kimal3-plan/.claude-plugin/plugin.json +7 -0
- package/claude-plugins/kimal3-plan/hooks/erinnerung.ts +79 -0
- package/claude-plugins/kimal3-plan/hooks/fehlerzeile.ts +41 -0
- package/claude-plugins/kimal3-plan/hooks/hooks.json +1 -0
- package/claude-plugins/kimal3-plan/hooks/lauf.ts +143 -0
- package/claude-plugins/kimal3-plan/hooks/lebenszeichen.ts +197 -0
- package/claude-plugins/kimal3-plan/hooks/ordner.ts +141 -0
- package/claude-plugins/kimal3-plan/hooks/register.tsx +22 -0
- package/claude-plugins/kimal3-plan/hooks/spec-text.ts +45 -0
- package/claude-plugins/kimal3-plan/types/index.d.ts +10 -0
- package/coach/de/kimal3-coach.md +33 -0
- package/coach/en/kimal3-coach.md +33 -0
- package/dist/argumente.js +12 -0
- package/dist/atomar.js +22 -0
- package/dist/befehle/einrichten.js +4 -4
- package/dist/befehle/login.js +2 -2
- package/dist/befehle/plan-hoch.js +8 -0
- package/dist/befehle/plan-melde.js +105 -0
- package/dist/befehle/plan-senden.js +149 -0
- package/dist/befehle/plan.js +45 -10
- package/dist/befehle/projekt-verbinden.js +141 -0
- package/dist/befehle/skill-install.js +2 -2
- package/dist/befehle-katalog-en.js +6 -6
- package/dist/befehle-katalog.js +8 -7
- package/dist/befehlswoerter.js +9 -0
- package/dist/einstellungen-schreiben.js +1 -1
- package/dist/kimal3.js +22 -0
- package/dist/meldungen-speicher.js +189 -0
- package/dist/{logo-ablegen.js β mods-ablegen.js} +87 -20
- package/dist/plugins-mischen.js +7 -2
- package/dist/projekt-ordner.js +189 -0
- package/dist/texte/einrichten.js +3 -1
- package/dist/texte/meldungen.js +127 -0
- package/dist/texte/plan.js +42 -22
- package/dist/texte/wache.js +4 -4
- package/dist/wache-rechnung.js +3 -1
- package/package.json +1 -1
- package/skill/commands/k3feierabend.md +20 -0
- package/skill/commands/k3plan.md +151 -211
- package/skill/en/commands/k3plan.md +157 -219
- package/skill/en/commands/k3wrapup.md +19 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meldungen an die Projektseite und die gemerkten, die noch nicht ankamen (Spec 2026-10-23-brett-in-echtzeit-design.md,
|
|
3
|
+
* 1.1, E-6, S-10, US-4; Bauplan R-4).
|
|
4
|
+
*
|
|
5
|
+
* π `Meldung` und `Umschlag` stehen hier Feld fuer Feld wie in `src/lib/planung/meldung.ts` -- `cli/` kann `src/` nicht
|
|
6
|
+
* importieren. `meldungFehler` urteilt wie `pruefeMeldung` dort; `check:cli` haelt beide an denselben Proben gegeneinander.
|
|
7
|
+
* Warum die CLI ueberhaupt selbst prueft: Die App lehnt eine Anfrage mit EINER unlesbaren Meldung ganz ab. Eine solche,
|
|
8
|
+
* offline gemerkt, hielte sonst jede spaetere Sendung auf -- fuer immer.
|
|
9
|
+
*
|
|
10
|
+
* Gemerkt wird unter `~/.kimal3/meldungen/` (ueberlebt das Schliessen der Sitzung, US-4), eine Datei je Meldung:
|
|
11
|
+
* `<entstanden ohne :>-<6 Zeichen>.json`. Mit drin: das Projekt, fuer das sie gedacht war, und der Ordner, aus dem sie
|
|
12
|
+
* kam -- sie wird auch aus einem anderen Ordner nachgeschickt, und eine Spec-Meldung nimmt ihren Text von dort mit.
|
|
13
|
+
*/
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { schreibeAtomar } from "./atomar.js";
|
|
19
|
+
/**
|
|
20
|
+
* Die Sitzung, aus der eine Meldung kommt: zuerst `KIMAL3_SITZUNG`, die der Mod `kimal3-plan` bei jedem Start setzt --
|
|
21
|
+
* auch nach `/clear`, wo ungemessen ist, ob `CLAUDE_CODE_SESSION_ID` neu ist (Ruling Final, Important 2). Ohne Mod
|
|
22
|
+
* Claudes eigene Variable. Nur fuer Meldungen: die Wache bleibt bei `sitzungJetzt`.
|
|
23
|
+
*/
|
|
24
|
+
export function sitzungFuerMeldung(umgebung) {
|
|
25
|
+
for (const wert of [umgebung.KIMAL3_SITZUNG, umgebung.CLAUDE_CODE_SESSION_ID]) {
|
|
26
|
+
if (typeof wert === "string" && wert.trim() !== "")
|
|
27
|
+
return wert;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const istObjekt = (w) => typeof w === "object" && w !== null && !Array.isArray(w);
|
|
32
|
+
const istText = (w) => typeof w === "string" && w.length > 0;
|
|
33
|
+
const istTextOderNull = (w) => w === null || istText(w);
|
|
34
|
+
const STAENDE = ["offen", "in_arbeit", "gebaut", "weggelassen"];
|
|
35
|
+
/** `null` = lesbar; sonst das Feld, das die App nennen wuerde (`meldung` oder `art`). Spiegel von `pruefeInhalt`. */
|
|
36
|
+
export function meldungFehler(m) {
|
|
37
|
+
if (!istObjekt(m))
|
|
38
|
+
return "meldung";
|
|
39
|
+
switch (m.art) {
|
|
40
|
+
case "spec":
|
|
41
|
+
if (!istText(m.schluessel) || !istText(m.titel) || !istTextOderNull(m.datei) || !Array.isArray(m.stories))
|
|
42
|
+
return "meldung";
|
|
43
|
+
return m.stories.every((s) => istObjekt(s) && istText(s.schluessel) && istText(s.titel)) ? null : "meldung";
|
|
44
|
+
case "story":
|
|
45
|
+
if (!istText(m.spec) || !istText(m.story) || typeof m.stand !== "string" || !STAENDE.includes(m.stand))
|
|
46
|
+
return "meldung";
|
|
47
|
+
if (m.runde !== undefined && !Number.isInteger(m.runde))
|
|
48
|
+
return "meldung";
|
|
49
|
+
if (m.kriterien !== undefined && !Array.isArray(m.kriterien))
|
|
50
|
+
return "meldung";
|
|
51
|
+
if (m.befunde !== undefined && !Array.isArray(m.befunde))
|
|
52
|
+
return "meldung";
|
|
53
|
+
return (m.testen === undefined || istTextOderNull(m.testen)) && (m.grund === undefined || istTextOderNull(m.grund)) ? null : "meldung";
|
|
54
|
+
case "thema": return istText(m.schluessel) && istText(m.text) ? null : "meldung";
|
|
55
|
+
case "uebernommen": return Number.isInteger(m.rueckmeldung) && istText(m.antwort) ? null : "meldung";
|
|
56
|
+
case "eingeordnet": return istText(m.schluessel) && istText(m.text) && istText(m.antwort) ? null : "meldung";
|
|
57
|
+
default: return "art";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ββ gemerkte Meldungen ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
61
|
+
const ORDNER = join(homedir(), ".kimal3", "meldungen");
|
|
62
|
+
export const dateiNameFuer = (entstanden, zufall) => `${entstanden.replace(/:/g, "")}-${zufall}.json`; // kein-anzeigetext: Dateiname
|
|
63
|
+
/**
|
|
64
|
+
* In der Reihenfolge, in der sie entstanden -- als ZEIT verglichen, nicht als Text (ein Versatz wie `+02:00` sortiert
|
|
65
|
+
* als Text falsch); bei gleicher Zeit nach Dateiname, damit die Reihenfolge nie vom Zufall des Dateisystems abhaengt.
|
|
66
|
+
*/
|
|
67
|
+
export function ordneGemerkte(liste) {
|
|
68
|
+
return [...liste].sort((a, b) => Date.parse(a.umschlag.entstanden) - Date.parse(b.umschlag.entstanden) || (a.datei < b.datei ? -1 : a.datei > b.datei ? 1 : 0));
|
|
69
|
+
}
|
|
70
|
+
/** Merkt eine Meldung fuer spaeter. `false`: nicht geschrieben -- dann ist sie verloren, und das muss man sagen. */
|
|
71
|
+
export function merke(projektId, ordner, u) {
|
|
72
|
+
try {
|
|
73
|
+
mkdirSync(ORDNER, { recursive: true });
|
|
74
|
+
const datei = dateiNameFuer(u.entstanden, randomBytes(3).toString("hex"));
|
|
75
|
+
schreibeAtomar(join(ORDNER, datei), `${JSON.stringify({ format: 1, projektId, ordner, umschlag: u }, null, 2)}\n`);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Alle gemerkten, in Sendereihenfolge -- und die Dateien, die sich nicht lesen lassen. Die bleiben liegen (geloescht
|
|
84
|
+
* wird nichts ungesehen), werden aber GENANNT: Still uebergangen saehe βnichts gemerktβ aus wie Erfolg.
|
|
85
|
+
*/
|
|
86
|
+
export function gemerkteMitUnlesbaren() {
|
|
87
|
+
let namen;
|
|
88
|
+
try {
|
|
89
|
+
namen = readdirSync(ORDNER).filter((n) => n.endsWith(".json"));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { liste: [], unlesbar: [] };
|
|
93
|
+
}
|
|
94
|
+
const unlesbar = [];
|
|
95
|
+
const liste = namen.flatMap((datei) => {
|
|
96
|
+
try {
|
|
97
|
+
const o = JSON.parse(readFileSync(join(ORDNER, datei), "utf8"));
|
|
98
|
+
if (istObjekt(o) && istText(o.projektId) && istObjekt(o.umschlag)) {
|
|
99
|
+
const u = o.umschlag;
|
|
100
|
+
if (istText(u.entstanden) && !Number.isNaN(Date.parse(u.entstanden)) && meldungFehler(u.meldung) === null) {
|
|
101
|
+
const sitzung = istText(u.sitzung) ? u.sitzung : null;
|
|
102
|
+
return [{ datei, projektId: o.projektId, ordner: istText(o.ordner) ? o.ordner : null, umschlag: { meldung: u.meldung, entstanden: u.entstanden, sitzung } }];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* unten gezaehlt */
|
|
108
|
+
}
|
|
109
|
+
unlesbar.push(datei);
|
|
110
|
+
return [];
|
|
111
|
+
});
|
|
112
|
+
return { liste: ordneGemerkte(liste), unlesbar: unlesbar.sort() };
|
|
113
|
+
}
|
|
114
|
+
export const gemerkte = () => gemerkteMitUnlesbaren().liste;
|
|
115
|
+
export const gemerktOrdner = () => ORDNER;
|
|
116
|
+
export function vergiss(datei) {
|
|
117
|
+
try {
|
|
118
|
+
rmSync(join(ORDNER, datei));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* schon weg */
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Absagen, die nur DIESE Anfrage betreffen (ihre Form) -- die naechste Anfrage desselben Projekts kann durchgehen. */
|
|
125
|
+
const NUR_DIESES_PAKET = ["rumpf", "zu-viele", "meldung-unlesbar"];
|
|
126
|
+
/**
|
|
127
|
+
* Was die Antwort fuer die Meldungen bedeutet. π βmerkenβ heisst: Ob sie ankamen, ist nicht sicher -- also nochmals
|
|
128
|
+
* schicken. Das ist harmlos: Dieselbe Meldung zweimal aendert nichts (E-6 vergleicht βspaeter alsβ, nicht βgleichβ).
|
|
129
|
+
* π΄ Endgueltig abgesagt ist eine Meldung NUR, wenn die App selbst mit einem `grund` antwortet (Ruling Fixrunde 1):
|
|
130
|
+
* Eine `413` vom Rumpf-Deckel von Vercel, eine `403`-Seite einer Firewall, eine `400` ohne JSON kamen nie bei der App an
|
|
131
|
+
* -- wer sie als Absage nimmt, loescht gemerkte Meldungen, die nie jemand geprueft hat. Darum: ohne `grund` βmerkenβ.
|
|
132
|
+
* Ebenso Aussetzer (5xx, 408, 429) und eine `200`, deren Rumpf nicht die erwartete Form hat (WLAN-Anmeldeportal).
|
|
133
|
+
*/
|
|
134
|
+
export function meldungenErgebnis(status, koerper) {
|
|
135
|
+
const k = istObjekt(koerper) ? koerper : null;
|
|
136
|
+
const grund = typeof k?.grund === "string" && k.grund !== "" ? k.grund : null;
|
|
137
|
+
const meldung = typeof k?.meldung === "string" ? k.meldung : "";
|
|
138
|
+
if (status === 200) {
|
|
139
|
+
const abgesagt = Array.isArray(k?.abgesagt) ? k.abgesagt : null;
|
|
140
|
+
const lesbar = abgesagt?.every((a) => istObjekt(a) && Number.isInteger(a.index) && typeof a.meldung === "string");
|
|
141
|
+
if (k === null || !Number.isInteger(k.angenommen) || abgesagt === null || !lesbar)
|
|
142
|
+
return { art: "merken" };
|
|
143
|
+
return { art: "ok", angenommen: k.angenommen, abgesagt: abgesagt };
|
|
144
|
+
}
|
|
145
|
+
if (status === 401)
|
|
146
|
+
return { art: "anmelden", meldung };
|
|
147
|
+
if (grund === null || status === 408 || status === 429 || status >= 500)
|
|
148
|
+
return { art: "merken" };
|
|
149
|
+
return { art: "absage", grund, meldung, umfang: NUR_DIESES_PAKET.includes(grund) ? "paket" : "projekt" };
|
|
150
|
+
}
|
|
151
|
+
// ββ In Paketen schicken (reine Ablaufregel; das Netz kommt als Funktion herein) ββββββββββββββββββββββββββββββββββ
|
|
152
|
+
export const JE_ANFRAGE = 50;
|
|
153
|
+
/**
|
|
154
|
+
* Schickt die Umschlaege eines Projekts in Paketen zu 50 (Ruling Task 7: eine Anfrage, nicht eine je Meldung).
|
|
155
|
+
* `je` hat dieselbe Laenge und Reihenfolge wie `umschlaege`; `offen` heisst: nicht sicher, ob sie ankam.
|
|
156
|
+
* Eine Absage, die das PROJEKT betrifft (geloescht, Planung unlesbar), gilt fuer alle restlichen Meldungen dieses
|
|
157
|
+
* Projekts; eine, die nur die Form DIESES Pakets betrifft, nur fuer dieses -- die naechsten Pakete gehen trotzdem.
|
|
158
|
+
*/
|
|
159
|
+
export async function sendeInPaketen(umschlaege, schicke) {
|
|
160
|
+
const je = umschlaege.map(() => ({ art: "offen" }));
|
|
161
|
+
let problem = null;
|
|
162
|
+
for (let von = 0; von < umschlaege.length; von += JE_ANFRAGE) {
|
|
163
|
+
const paket = umschlaege.slice(von, von + JE_ANFRAGE);
|
|
164
|
+
const v = await schicke(paket);
|
|
165
|
+
if (v === "netz")
|
|
166
|
+
return { je, problem: { art: "nicht-erreichbar" } };
|
|
167
|
+
if (v === "nicht-angemeldet")
|
|
168
|
+
return { je, problem: { art: "anmelden", meldung: "" } };
|
|
169
|
+
const e = meldungenErgebnis(v.status, v.koerper);
|
|
170
|
+
if (e.art === "merken")
|
|
171
|
+
return { je, problem: { art: "nicht-erreichbar" } };
|
|
172
|
+
if (e.art === "anmelden")
|
|
173
|
+
return { je, problem: { art: "anmelden", meldung: e.meldung } };
|
|
174
|
+
if (e.art === "absage") {
|
|
175
|
+
const bis = e.umfang === "projekt" ? umschlaege.length : von + paket.length;
|
|
176
|
+
for (let i = von; i < bis; i++)
|
|
177
|
+
je[i] = { art: "abgesagt", meldung: e.meldung };
|
|
178
|
+
problem = { art: "absage", grund: e.grund, meldung: e.meldung };
|
|
179
|
+
if (e.umfang === "projekt")
|
|
180
|
+
return { je, problem };
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
paket.forEach((_, i) => { je[von + i] = { art: "angenommen" }; });
|
|
184
|
+
for (const a of e.abgesagt)
|
|
185
|
+
if (a.index >= 0 && a.index < paket.length)
|
|
186
|
+
je[von + a.index] = { art: "abgesagt", meldung: a.meldung };
|
|
187
|
+
}
|
|
188
|
+
return { je, problem };
|
|
189
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
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";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { basename, dirname, join, posix, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { EINRICHTEN_TEXTE } from "./texte/einrichten.js";
|
|
6
|
+
import { eigeneVersion } from "./version.js";
|
|
6
7
|
/**
|
|
7
|
-
* Legt den Plugin-Katalog mit
|
|
8
|
-
* Sprache des Kontos. Claude Code liest
|
|
8
|
+
* Legt den Plugin-Katalog mit den beiden Mods (Logo und Board) in `~/.kimal3/claude-plugins` ab und setzt dort die
|
|
9
|
+
* Sprache des Kontos, im Logo-Mod dazu die Ablage (`mitAblage`). Claude Code liest die Mods aus genau diesem Ordner (Katalog `kimal3`,
|
|
9
10
|
* `plugins-mischen.ts`).
|
|
10
11
|
*
|
|
11
12
|
* π Warum ein fester Ordner im Benutzerordner und nicht der Ordner im installierten npm-Paket:
|
|
@@ -16,6 +17,9 @@ import { EINRICHTEN_TEXTE } from "./texte/einrichten.js";
|
|
|
16
17
|
export function katalogOrdner() {
|
|
17
18
|
return join(homedir(), ".kimal3", "claude-plugins"); // kein-anzeigetext: Ordnername
|
|
18
19
|
}
|
|
20
|
+
/** Die Dateien der Mods, die die Sprach-Zeile tragen (relativ zum Katalog); nur der Logo-Mod trΓ€gt auch die Ablage-Zeile. */
|
|
21
|
+
const LOGO_MOD = join("kimal3-logo", "hooks", "register.tsx"); // kein-anzeigetext: Dateipfad
|
|
22
|
+
const MODS = [LOGO_MOD, join("kimal3-plan", "hooks", "register.tsx")]; // kein-anzeigetext: Dateipfade
|
|
19
23
|
/** Die Zeile im Mod, die die Sprache festlegt -- `check:logo` prΓΌft, dass es sie genau einmal gibt. */
|
|
20
24
|
export const SPRACH_ZEILE = "const SPRACHE: 'de' | 'en' = 'de'"; // kein-anzeigetext: Quelltext des Mods
|
|
21
25
|
/** Reine Rechnung: der Mod-Text in der Sprache des Kontos, oder `null`, wenn die Zeile fehlt. */
|
|
@@ -24,10 +28,64 @@ export function mitSprache(quelltext, sprache) {
|
|
|
24
28
|
return null;
|
|
25
29
|
return quelltext.replace(SPRACH_ZEILE, `const SPRACHE: 'de' | 'en' = '${sprache}'`); // kein-anzeigetext: Quelltext des Mods
|
|
26
30
|
}
|
|
31
|
+
/** Die Zeile im Mod, die beim Ablegen die Ablage bekommt; im Paket steht dort die leere. */
|
|
32
|
+
export const ABLAGE_ZEILE = "const ABLAGE: Ablage = KEINE_ABLAGE"; // kein-anzeigetext: Quelltext des Mods
|
|
33
|
+
/**
|
|
34
|
+
* Reine Rechnung: der Mod-Text mit der Ablage, oder `null`, wenn die Zeile nicht genau einmal dasteht. Ohne Ablage
|
|
35
|
+
* (keine eigene Fassung) bleibt die leere Zeile stehen -- der Mod zeigt dann wie bisher nur das Logo (U-9).
|
|
36
|
+
* π΄ `split`/`join`, nie `replace(zeile, text)`: Im Ersatz-Text eines `replace` sind `$&` und `$'` Befehle, und ein
|
|
37
|
+
* Ordnername darf beides enthalten -- dann stΓΌnde fremder Quelltext im Mod (messen-und-pruefen Β§23, U-16). `JSON.stringify`
|
|
38
|
+
* macht aus jedem Wert ein gΓΌltiges StΓΌck Quelltext, auch mit `"` und `\` (Windows-Pfade).
|
|
39
|
+
*/
|
|
40
|
+
export function mitAblage(quelltext, ablage) {
|
|
41
|
+
const teile = quelltext.split(ABLAGE_ZEILE);
|
|
42
|
+
if (teile.length !== 2)
|
|
43
|
+
return null;
|
|
44
|
+
return ablage === null ? quelltext : teile.join(`const ABLAGE: Ablage = ${JSON.stringify(ablage)}`); // kein-anzeigetext: Quelltext des Mods
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reine Rechnung hinter `ablageWerte`. Ohne eigene Fassung keine Ablage (`null`).
|
|
48
|
+
* `npm` ist das `npm-cli.js`, das zu DIESEM Node gehΓΆrt (Windows daneben, sonst unter `lib/`), und nur dann gesetzt, wenn
|
|
49
|
+
* es die Datei gibt und `kimal3` global installiert lΓ€uft. Sonst `""`: Der Knopf bittet dann Claude (U-10), statt ein
|
|
50
|
+
* `npm install -g` anzustoΓen, nach dem `kimal3 --version` doch die alte Zahl sagte.
|
|
51
|
+
* π Global heiΓt, als Erlaubnisliste (messen-und-pruefen Β§23): `β¦/node_modules/kimal3/β¦` mit genau EINEM `node_modules`,
|
|
52
|
+
* ohne `_npx` (der Zwischenspeicher von `npx`), und ΓΌber dem `node_modules` kein `package.json` (sonst ist es der Ordner
|
|
53
|
+
* eines Projekts). Ein Start aus dem Repo (`cli/dist`) ist damit auch nicht global.
|
|
54
|
+
*/
|
|
55
|
+
export function ablageAus(lage) {
|
|
56
|
+
if (lage.fassung === null)
|
|
57
|
+
return null;
|
|
58
|
+
const p = lage.system === "win32" ? win32 : posix;
|
|
59
|
+
const npm = lage.system === "win32"
|
|
60
|
+
? p.join(p.dirname(lage.node), "node_modules", "npm", "bin", "npm-cli.js") // kein-anzeigetext: Pfad
|
|
61
|
+
: p.join(p.dirname(p.dirname(lage.node)), "lib", "node_modules", "npm", "bin", "npm-cli.js"); // kein-anzeigetext: Pfad
|
|
62
|
+
const teile = lage.kimal3.split(/[\\/]/);
|
|
63
|
+
const i = teile.indexOf("node_modules");
|
|
64
|
+
const global = i > 0 && teile.lastIndexOf("node_modules") === i && teile[i + 1] === "kimal3" && !teile.includes("_npx")
|
|
65
|
+
&& !lage.gibtEs(p.join(teile.slice(0, i).join(p.sep), "package.json"));
|
|
66
|
+
return {
|
|
67
|
+
fassung: lage.fassung,
|
|
68
|
+
node: lage.node,
|
|
69
|
+
npm: global && lage.gibtEs(npm) ? npm : "",
|
|
70
|
+
kimal3: lage.kimal3,
|
|
71
|
+
kimal3Ordner: p.join(lage.heim, ".kimal3"), // kein-anzeigetext: Ordnername
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Die Ablage dieses Rechners, im laufenden `kimal3` gemessen (Bauplan 2026-10-23, Tabelle in 1.1). */
|
|
75
|
+
export function ablageWerte() {
|
|
76
|
+
return ablageAus({
|
|
77
|
+
fassung: eigeneVersion(),
|
|
78
|
+
node: process.execPath,
|
|
79
|
+
kimal3: fileURLToPath(new URL("./kimal3.js", import.meta.url)), // kein-anzeigetext: Dateiname
|
|
80
|
+
heim: homedir(),
|
|
81
|
+
system: platform(),
|
|
82
|
+
gibtEs: existsSync,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
27
85
|
/**
|
|
28
86
|
* π΄ NIE EIN HALBER ORDNER (Durchsicht-Funde 2 und 9): Claudes Einstellungen zeigen auf `katalogOrdner()`,
|
|
29
87
|
* und eine laufende Claude-Sitzung hat den Mod daraus geladen. Deshalb:
|
|
30
|
-
* 1. Sprache setzen, BEVOR irgendetwas angefasst wird -- fehlt
|
|
88
|
+
* 1. Sprache und Ablage setzen, BEVOR irgendetwas angefasst wird -- fehlt eine der Zeilen, bleibt alles, wie es war.
|
|
31
89
|
* 2. Steht schon genau das da, was entstehen soll: nichts anfassen (Lage P-8). Das ist auch der Normalfall
|
|
32
90
|
* eines zweiten Laufs, und so kommt er gar nicht erst in die NΓ€he der Windows-Sperren unten.
|
|
33
91
|
* 3. Den neuen Ordner daneben fertig bauen (`β¦.neu`), dann tauschen: alten Ordner wegschieben, neuen an
|
|
@@ -39,17 +97,23 @@ export function mitSprache(quelltext, sprache) {
|
|
|
39
97
|
* den der Aufrufer in einer Zeile nennt. Unter Windows hΓ€lt ihn ein Programm fest, dessen Arbeitsordner darin liegt;
|
|
40
98
|
* Claude selbst tut es nicht (gemessen 18.10. bei acht laufenden Claude-Prozessen, Spec 2026-10-18, Abschnitt 1.2).
|
|
41
99
|
*/
|
|
42
|
-
export function
|
|
43
|
-
// `dist/
|
|
100
|
+
export function modsAblegen(sprache) {
|
|
101
|
+
// `dist/mods-ablegen.js` -> `../claude-plugins` im installierten Paket (`files` in package.json).
|
|
44
102
|
const quelle = fileURLToPath(new URL("../claude-plugins", import.meta.url));
|
|
45
103
|
const ziel = katalogOrdner();
|
|
46
104
|
const neu = `${ziel}.neu`; // kein-anzeigetext: Ordnername
|
|
47
|
-
const modDatei = (ordner) => join(ordner, "kimal3-logo", "hooks", "register.tsx");
|
|
48
105
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
106
|
+
// Beide Mods bekommen die Sprache des Kontos, der Logo-Mod dazu die Ablage fΓΌr den Update-Knopf (nur er trΓ€gt die
|
|
107
|
+
// Zeile); fehlt eine Zeile, bleibt alles, wie es war.
|
|
108
|
+
const texte = new Map();
|
|
109
|
+
for (const mod of MODS) {
|
|
110
|
+
const mitSpr = mitSprache(readFileSync(join(quelle, mod), "utf8"), sprache);
|
|
111
|
+
const text = mitSpr === null ? null : mod === LOGO_MOD ? mitAblage(mitSpr, ablageWerte()) : mitSpr;
|
|
112
|
+
if (text === null)
|
|
113
|
+
return { ok: false, detail: join(quelle, mod) };
|
|
114
|
+
texte.set(mod, text);
|
|
115
|
+
}
|
|
116
|
+
if (stehtSchonDa(quelle, ziel, texte)) {
|
|
53
117
|
// Auch im Normalfall aufrΓ€umen, samt einem halb gebauten `β¦.neu`: sonst blieben Reste fΓΌr immer (Durchsicht 3/4).
|
|
54
118
|
rmSync(neu, { recursive: true, force: true });
|
|
55
119
|
resteWegraeumen(ziel);
|
|
@@ -57,7 +121,8 @@ export function logoAblegen(sprache) {
|
|
|
57
121
|
}
|
|
58
122
|
rmSync(neu, { recursive: true, force: true });
|
|
59
123
|
cpSync(quelle, neu, { recursive: true });
|
|
60
|
-
|
|
124
|
+
for (const [mod, text] of texte)
|
|
125
|
+
writeFileSync(join(neu, mod), text, "utf8");
|
|
61
126
|
if (!existsSync(ziel)) {
|
|
62
127
|
renameSync(neu, ziel);
|
|
63
128
|
return { ok: true, ordner: ziel, stand: "neu" };
|
|
@@ -85,15 +150,15 @@ export function logoAblegen(sprache) {
|
|
|
85
150
|
}
|
|
86
151
|
}
|
|
87
152
|
/**
|
|
88
|
-
* FΓΌr `login` und `skill install` (Spec 2026-10-18, Abschnitt 1.2):
|
|
153
|
+
* FΓΌr `login` und `skill install` (Spec 2026-10-18, Abschnitt 1.2): VORHANDENE Mods auffrischen (Update,
|
|
89
154
|
* Sprachwechsel), nie eines anlegen -- das darf nur `einrichten`, wie beim Coach (`frischeCoachAuf`). Wer das Logo nie
|
|
90
155
|
* eingerichtet hat, hΓΆrt nichts und bekommt keinen Ordner (L-2). Eine Zeile nur, wenn getauscht wurde, das alte bleiben
|
|
91
156
|
* musste oder etwas scheiterte; sonst Stille. Wirft nie und Γ€ndert den RΓΌckgabewert des Befehls nicht (L-6).
|
|
92
157
|
*/
|
|
93
|
-
export function
|
|
158
|
+
export function frischeModsAuf(s) {
|
|
94
159
|
if (!existsSync(katalogOrdner()))
|
|
95
160
|
return;
|
|
96
|
-
const ablage =
|
|
161
|
+
const ablage = modsAblegen(s);
|
|
97
162
|
const t = EINRICHTEN_TEXTE[s];
|
|
98
163
|
if (!ablage.ok)
|
|
99
164
|
console.log(t.logoNotWritten(ablage.detail));
|
|
@@ -102,16 +167,18 @@ export function frischeLogoAuf(s) {
|
|
|
102
167
|
else if (ablage.stand === "alt")
|
|
103
168
|
console.log(t.logoNotRefreshed);
|
|
104
169
|
}
|
|
105
|
-
/** Gleiche Dateien, gleicher Inhalt --
|
|
106
|
-
function stehtSchonDa(quelle, ziel,
|
|
170
|
+
/** Gleiche Dateien, gleicher Inhalt -- die Mods mit Sprache (und im Logo-Mod Ablage), alles andere wie im Paket. */
|
|
171
|
+
function stehtSchonDa(quelle, ziel, modTexte) {
|
|
107
172
|
if (!existsSync(ziel))
|
|
108
173
|
return false;
|
|
109
174
|
const dateien = (ordner) => alleDateien(ordner).sort();
|
|
110
175
|
const soll = dateien(quelle);
|
|
111
176
|
if (JSON.stringify(soll) !== JSON.stringify(dateien(ziel)))
|
|
112
177
|
return false;
|
|
113
|
-
|
|
114
|
-
|
|
178
|
+
return soll.every((d) => {
|
|
179
|
+
const modText = modTexte.get(d);
|
|
180
|
+
return (modText === undefined ? readFileSync(join(quelle, d)) : Buffer.from(modText, "utf8")).equals(readFileSync(join(ziel, d)));
|
|
181
|
+
});
|
|
115
182
|
}
|
|
116
183
|
/**
|
|
117
184
|
* Alle Dateien unter `ordner`, als Pfade relativ dazu. π Von Hand statt `readdirSync(β¦, { recursive: true })`: Das kann
|
package/dist/plugins-mischen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { istObjekt, leseEinstellungen } from "./einstellungen-mischen.js";
|
|
2
2
|
/**
|
|
3
|
-
* Traegt superpowers
|
|
3
|
+
* Traegt superpowers, das KImal3-Logo und das Board (kimal3-plan) in Claudes Einstellungen ein -- als Katalog plus
|
|
4
4
|
* Einschalter, so wie Anthropic Firmen-Mods auf jeden Rechner verteilt (Bauplan 2026-10-15,
|
|
5
5
|
* βWarum anders als die Spec"). Beim naechsten Start laedt Claude Code beides selbst.
|
|
6
6
|
*
|
|
@@ -10,6 +10,7 @@ import { istObjekt, leseEinstellungen } from "./einstellungen-mischen.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export const KATALOG = "kimal3"; // kein-anzeigetext: Name des Plugin-Katalogs in Claudes Einstellungen
|
|
12
12
|
export const LOGO = `kimal3-logo@${KATALOG}`; // kein-anzeigetext: Plugin-Kennung
|
|
13
|
+
export const PLAN = `kimal3-plan@${KATALOG}`; // kein-anzeigetext: Plugin-Kennung
|
|
13
14
|
const OFFIZIELL = "claude-plugins-official"; // kein-anzeigetext: Anthropics Katalog
|
|
14
15
|
export const SUPERPOWERS = `superpowers@${OFFIZIELL}`; // kein-anzeigetext: Plugin-Kennung
|
|
15
16
|
/**
|
|
@@ -47,6 +48,9 @@ export function mischePlugins(alt, katalogPfad, mitSuperpowers = true) {
|
|
|
47
48
|
neueKataloge[KATALOG] = zeigtSchonHin ? alterEintrag : { ...alterEintrag, source: { source: "directory", path: katalogPfad } };
|
|
48
49
|
if (!(LOGO in plugins))
|
|
49
50
|
neuePlugins[LOGO] = true;
|
|
51
|
+
// Wie beim Logo: ein Eintrag -- auch `false` -- heiΓt, er hat entschieden (US-7); nur was fehlt, wird eingeschaltet.
|
|
52
|
+
if (!(PLAN in plugins))
|
|
53
|
+
neuePlugins[PLAN] = true;
|
|
50
54
|
}
|
|
51
55
|
if (mitSuperpowers && !Object.keys(plugins).some((k) => k.startsWith("superpowers@"))) {
|
|
52
56
|
neuePlugins[SUPERPOWERS] = true;
|
|
@@ -69,10 +73,11 @@ export function pluginStand(inhalt) {
|
|
|
69
73
|
const lesen = leseEinstellungen(inhalt);
|
|
70
74
|
const plugins = lesen.ok ? lesen.objekt.enabledPlugins : undefined;
|
|
71
75
|
if (!istObjekt(plugins))
|
|
72
|
-
return { superpowers: "fehlt", logo: "fehlt" };
|
|
76
|
+
return { superpowers: "fehlt", logo: "fehlt", plan: "fehlt" };
|
|
73
77
|
const superpowers = Object.entries(plugins).filter(([k]) => k.startsWith("superpowers@")).map(([, v]) => v);
|
|
74
78
|
return {
|
|
75
79
|
superpowers: superpowers.includes(true) ? "an" : superpowers.length > 0 ? "aus" : "fehlt",
|
|
76
80
|
logo: plugins[LOGO] === true ? "an" : LOGO in plugins ? "aus" : "fehlt",
|
|
81
|
+
plan: plugins[PLAN] === true ? "an" : PLAN in plugins ? "aus" : "fehlt",
|
|
77
82
|
};
|
|
78
83
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zu welchem Projekt in der App gehoert dieser Ordner? (Spec 2026-10-23-brett-in-echtzeit-design.md, 1.4, 1.7, S-27
|
|
3
|
+
* bis S-31, US-8). Die Regel ist eine reine Rechnung (`ordnerlage`, `hauptordnerFuer`): Sie bekommt, was es auf der
|
|
4
|
+
* Platte gibt, als zwei Funktionen -- so prueft `check:cli` sie gegen die Tabelle in `scripts/projektordner-faelle.mts`,
|
|
5
|
+
* dieselbe, die der Mod (Task 8) in seine Tests uebernimmt.
|
|
6
|
+
*
|
|
7
|
+
* Projektordner: ein Ordner DIREKT in `werkbank-β¦/projekte/` (englisch `workbench-β¦/projects/`, auch gemischt), wie
|
|
8
|
+
* bei `/k3start`; der Beispielordner zaehlt nicht. Eine Arbeitskopie gehoert zum Projekt ihres Hauptordners -- auch
|
|
9
|
+
* eine, die NEBEN ihm in `projekte/` liegt (sonst waere `kimal3-app-board` ein eigenes Projekt).
|
|
10
|
+
*
|
|
11
|
+
* Der Ordnerzettel `plan/projekt.json` traegt Nummer und Titel des Projekts und kommt mit Git mit. Fehlt er, gilt
|
|
12
|
+
* hoechstens noch `projekt_id` aus `plan/stand.json` -- nur, um den Zettel daraus anzulegen (1.4).
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, statSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { schreibeAtomar } from "./atomar.js";
|
|
16
|
+
import { werkbankSpracheAus } from "./werkbank-name.js";
|
|
17
|
+
export const ZETTEL_PFAD = "plan/projekt.json"; // kein-anzeigetext: Dateipfad
|
|
18
|
+
const STAND_PFAD = "plan/stand.json"; // kein-anzeigetext: Dateipfad
|
|
19
|
+
const BEISPIELE = ["beispiel-rechnungen-erfassen", "example-capture-invoices"];
|
|
20
|
+
const PROJEKTORDNER = ["projekte", "projects"];
|
|
21
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
const normiert = (pfad) => pfad.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
23
|
+
/** Der Ordner und alle darueber, naechster zuerst (wie `ordnerNachOben` im Logo-Mod). */
|
|
24
|
+
const nachOben = (pfad) => {
|
|
25
|
+
const teile = normiert(pfad).split("/");
|
|
26
|
+
return teile.map((_, i) => teile.slice(0, teile.length - i).join("/")).filter((p) => p !== "");
|
|
27
|
+
};
|
|
28
|
+
/** `werkbank-x/projekte/<name>` irgendwo im Pfad -- der aeusserste Treffer, damit `.claude/worktrees/x` nicht zaehlt. */
|
|
29
|
+
function projektordnerIn(pfad) {
|
|
30
|
+
const teile = normiert(pfad).split("/");
|
|
31
|
+
for (let i = 0; i + 2 < teile.length; i++) {
|
|
32
|
+
if (werkbankSpracheAus(teile[i]) === null || !PROJEKTORDNER.includes(teile[i + 1]))
|
|
33
|
+
continue;
|
|
34
|
+
const name = teile[i + 2];
|
|
35
|
+
return name === "" || BEISPIELE.includes(name) ? null : { ordner: teile.slice(0, i + 3).join("/"), name };
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const ohneBom = (text) => (text.startsWith("ο»Ώ") ? text.slice(1) : text);
|
|
40
|
+
function objektAus(text) {
|
|
41
|
+
if (text === null)
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const j = JSON.parse(ohneBom(text));
|
|
45
|
+
return j !== null && typeof j === "object" && !Array.isArray(j) ? j : null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Der Zettel, wenn er gueltig ist -- sonst `null` (kein JSON, andere Formatnummer, keine Projektnummer). */
|
|
52
|
+
export function zettelAus(text) {
|
|
53
|
+
const o = objektAus(text);
|
|
54
|
+
if (o === null || o.format !== 1 || typeof o.projekt_id !== "string" || !UUID.test(o.projekt_id))
|
|
55
|
+
return null;
|
|
56
|
+
if (typeof o.titel !== "string" || o.titel.trim() === "")
|
|
57
|
+
return null;
|
|
58
|
+
return { format: 1, projekt_id: o.projekt_id, titel: o.titel };
|
|
59
|
+
}
|
|
60
|
+
export function zettelText(z) {
|
|
61
|
+
return `${JSON.stringify({ format: z.format, projekt_id: z.projekt_id, titel: z.titel }, null, 2)}\n`;
|
|
62
|
+
}
|
|
63
|
+
/** Aus einer alten `plan/stand.json` nur die Nummer (und der Titel, falls er dasteht). */
|
|
64
|
+
function zettelAusStand(text, name) {
|
|
65
|
+
const o = objektAus(text);
|
|
66
|
+
if (o === null || typeof o.projekt_id !== "string" || !UUID.test(o.projekt_id))
|
|
67
|
+
return null;
|
|
68
|
+
const titel = typeof o.projekt === "string" && o.projekt.trim() !== "" ? o.projekt : name;
|
|
69
|
+
return { format: 1, projekt_id: o.projekt_id, titel };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Der Hauptordner einer Git-Arbeitskopie: die naechste `.git` ueber `cwd`. Ist sie eine Datei mit
|
|
73
|
+
* `gitdir: β¦/.git/worktrees/<name>`, ist der Ordner davor der Hauptordner; sonst `null` (normales Projekt,
|
|
74
|
+
* Untermodul, kein Git). Dieselbe Erkennung wie `hauptordnerAusGitdatei` im Logo-Mod.
|
|
75
|
+
* Ein RELATIVER Pfad (`git worktree add` mit `worktree.useRelativePaths`: `gitdir: ../kimal3-app/.git/worktrees/x`)
|
|
76
|
+
* gilt ab der Wurzel der Arbeitskopie, nicht ab dem Startordner (Fixrunde 1).
|
|
77
|
+
*/
|
|
78
|
+
export function hauptordnerFuer(cwd, gibt, lies) {
|
|
79
|
+
const wurzel = nachOben(cwd).find((o) => gibt(`${o}/.git`)); // kein-anzeigetext: Dateipfad
|
|
80
|
+
if (wurzel === undefined)
|
|
81
|
+
return null;
|
|
82
|
+
const text = lies(`${wurzel}/.git`); // kein-anzeigetext: Dateipfad
|
|
83
|
+
const treffer = text === null ? null : /^gitdir:\s*(.+?)[\\/]\.git[\\/]worktrees[\\/][^\\/]+\s*$/.exec(text.trim());
|
|
84
|
+
if (treffer === null)
|
|
85
|
+
return null;
|
|
86
|
+
const ziel = normiert(treffer[1]);
|
|
87
|
+
return /^([A-Za-z]:)?\//.test(ziel) ? ziel : aufgeloest(`${wurzel}/${ziel}`);
|
|
88
|
+
}
|
|
89
|
+
/** `.` und `..` in einem Pfad mit `/` aufloesen, ohne die Platte zu fragen (die Wurzel bleibt stehen). */
|
|
90
|
+
function aufgeloest(pfad) {
|
|
91
|
+
const teile = [];
|
|
92
|
+
for (const t of pfad.split("/")) {
|
|
93
|
+
if (t === "." || (t === "" && teile.length > 0))
|
|
94
|
+
continue;
|
|
95
|
+
if (t === ".." && teile.length > 1)
|
|
96
|
+
teile.pop();
|
|
97
|
+
else if (t !== "..")
|
|
98
|
+
teile.push(t);
|
|
99
|
+
}
|
|
100
|
+
return teile.join("/");
|
|
101
|
+
}
|
|
102
|
+
export function ordnerlage(cwd, gibt, lies, hauptordner) {
|
|
103
|
+
// In einer Arbeitskopie zaehlt das Projekt des Hauptordners; der Zettel gehoert in ihre eigene Wurzel.
|
|
104
|
+
const projekt = projektordnerIn(hauptordner ?? cwd);
|
|
105
|
+
if (projekt === null)
|
|
106
|
+
return { art: "kein-projektordner" };
|
|
107
|
+
const ordner = hauptordner === null ? projekt.ordner : nachOben(cwd).find((o) => gibt(`${o}/.git`)) ?? projekt.ordner; // kein-anzeigetext: Dateipfad
|
|
108
|
+
const orte = ordner === projekt.ordner ? [ordner] : [ordner, projekt.ordner];
|
|
109
|
+
for (const o of orte) {
|
|
110
|
+
const zettel = zettelAus(lies(`${o}/${ZETTEL_PFAD}`));
|
|
111
|
+
if (zettel !== null)
|
|
112
|
+
return { art: "projekt", ordner, name: projekt.name, zettel, ausStandJson: false };
|
|
113
|
+
}
|
|
114
|
+
for (const o of orte) {
|
|
115
|
+
const zettel = zettelAusStand(lies(`${o}/${STAND_PFAD}`), projekt.name);
|
|
116
|
+
if (zettel !== null)
|
|
117
|
+
return { art: "projekt", ordner, name: projekt.name, zettel, ausStandJson: true };
|
|
118
|
+
}
|
|
119
|
+
return { art: "projekt", ordner, name: projekt.name, zettel: null, ausStandJson: false };
|
|
120
|
+
}
|
|
121
|
+
// ββ Platte ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
122
|
+
const gibtWirklich = (pfad) => {
|
|
123
|
+
try {
|
|
124
|
+
statSync(pfad);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const liesWirklich = (pfad) => {
|
|
132
|
+
try {
|
|
133
|
+
return readFileSync(pfad, "utf8");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return null; // fehlt, oder ein Ordner (`.git` eines normalen Projekts)
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/** Die Lage des Ordners, in dem `kimal3` gerade laeuft. */
|
|
140
|
+
export function ordnerlageJetzt() {
|
|
141
|
+
const cwd = process.cwd();
|
|
142
|
+
return ordnerlage(cwd, gibtWirklich, liesWirklich, hauptordnerFuer(cwd, gibtWirklich, liesWirklich));
|
|
143
|
+
}
|
|
144
|
+
/** Schreibt den Zettel. `false` heisst: nicht geschrieben (Schreibschutz, Ordner weg). */
|
|
145
|
+
export function schreibeZettel(ordner, z) {
|
|
146
|
+
try {
|
|
147
|
+
mkdirSync(`${ordner}/plan`, { recursive: true }); // kein-anzeigetext: Dateipfad
|
|
148
|
+
schreibeAtomar(`${ordner}/${ZETTEL_PFAD}`, zettelText(z));
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const textFeld = (k, name) => k !== null && typeof k === "object" && typeof k[name] === "string" ? k[name] : null;
|
|
156
|
+
/**
|
|
157
|
+
* Was die Antwort bedeutet. π Eine `404` ist nur mit dem Grund `projekt-geloescht` ein geloeschtes Projekt -- ohne
|
|
158
|
+
* Grund kennt die App die Route noch nicht (B-7), und dann darf Claude nicht βSoll ich ein neues anlegen?β fragen.
|
|
159
|
+
*/
|
|
160
|
+
export function verbindenErgebnis(status, koerper) {
|
|
161
|
+
const grund = textFeld(koerper, "grund");
|
|
162
|
+
if (status === 200) {
|
|
163
|
+
const projektId = textFeld(koerper, "projektId"); // kein-anzeigetext: Feldname der Antwort
|
|
164
|
+
const titel = textFeld(koerper, "titel");
|
|
165
|
+
const angelegt = koerper?.angelegt;
|
|
166
|
+
return projektId !== null && UUID.test(projektId) && titel !== null && typeof angelegt === "boolean"
|
|
167
|
+
? { art: "verbunden", projektId, titel, angelegt } : { art: "unlesbar" };
|
|
168
|
+
}
|
|
169
|
+
if (status === 404 && grund === "projekt-geloescht")
|
|
170
|
+
return { art: "geloescht" };
|
|
171
|
+
if (status === 409 && grund === "projekt-mehrdeutig") {
|
|
172
|
+
const roh = koerper.kandidaten;
|
|
173
|
+
const kandidaten = Array.isArray(roh)
|
|
174
|
+
? roh.flatMap((x) => {
|
|
175
|
+
const projektId = textFeld(x, "projektId"); // kein-anzeigetext: Feldname der Antwort
|
|
176
|
+
const titel = textFeld(x, "titel");
|
|
177
|
+
return projektId !== null && titel !== null ? [{ projektId, titel }] : [];
|
|
178
|
+
})
|
|
179
|
+
: [];
|
|
180
|
+
return { art: "mehrdeutig", meldung: textFeld(koerper, "meldung") ?? "", kandidaten };
|
|
181
|
+
}
|
|
182
|
+
if (status === 409 && grund === "kein-profil")
|
|
183
|
+
return { art: "kein-profil", meldung: textFeld(koerper, "meldung") ?? "" };
|
|
184
|
+
// Die Firmenwahl kommt in der Form von `projekt neu` (mit `firmen`/`auswahl`); jede andere 409 ist ein fertiger Satz.
|
|
185
|
+
const k = koerper;
|
|
186
|
+
if (status === 409 && (Array.isArray(k?.firmen) || Array.isArray(k?.auswahl)))
|
|
187
|
+
return { art: "firma" };
|
|
188
|
+
return { art: "sonst" };
|
|
189
|
+
}
|
package/dist/texte/einrichten.js
CHANGED
|
@@ -42,6 +42,7 @@ const de = {
|
|
|
42
42
|
noWorkbank: "In der Lieferung steckt keine Werkbank. Bitte melde das an kimal3.",
|
|
43
43
|
// Plugins (Bauplan 2026-10-15): Claude liest die Zeile und sagt der Kundin, was nach dem Neustart erscheint.
|
|
44
44
|
logoName: "KImal3-Logo",
|
|
45
|
+
planName: "KImal3-Board",
|
|
45
46
|
// `an`: eingeschaltet Β· `aus`: von ihr selbst ausgeschaltet (bleibt so, die Anleitung schickt sie dann nicht ins MenΓΌ)
|
|
46
47
|
// Β· `neu`: gerade eingetragen (kommt mit dem Neustart) oder stand schon da (Durchsicht-Funde 1 und 7).
|
|
47
48
|
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."}` +
|
|
@@ -57,7 +58,7 @@ const de = {
|
|
|
57
58
|
// Die Sperre setzt ein anderes Programm mit Arbeitsordner im Logo-Ordner, nicht Claude -- deshalb kein Rat, Claude zu
|
|
58
59
|
// beenden (fΓΌr App-Nutzer ein Kreis; Spec 2026-10-18, Abschnitt 1.2).
|
|
59
60
|
logoOldVersion: " Das Logo konnte gerade nicht aufgefrischt werden, weil ein anderes Programm seinen Ordner benutzt. Es kommt beim nΓ€chsten `kimal3 einrichten`.",
|
|
60
|
-
// Auffrischen bei `login` und `skill install` (`
|
|
61
|
+
// Auffrischen bei `login` und `skill install` (`frischeModsAuf`); scheitert es, sagt es `logoNotWritten`.
|
|
61
62
|
logoRefreshed: " Das Logo ΓΌber dem Eingabefeld ist aufgefrischt. Ist es eingeschaltet, siehst du es nach dem nΓ€chsten Neustart von Claude.",
|
|
62
63
|
logoNotRefreshed: " Das Logo konnte gerade nicht aufgefrischt werden, weil ein anderes Programm seinen Ordner benutzt. Es kommt beim nΓ€chsten `kimal3 skill install`.",
|
|
63
64
|
};
|
|
@@ -98,6 +99,7 @@ const en = {
|
|
|
98
99
|
unsafeFolder: (name) => `The server sent a folder name I won't create: ${name}. Please report this to kimal3.`,
|
|
99
100
|
noWorkbank: "The delivery contains no workbench. Please report this to kimal3.",
|
|
100
101
|
logoName: "KImal3 logo",
|
|
102
|
+
planName: "KImal3 board",
|
|
101
103
|
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."}` +
|
|
102
104
|
(aus.length > 0 ? ` Switched off by you, stays off: ${aus.join(", ")}.` : ""),
|
|
103
105
|
pluginsNotWritten: (grund) => ` ${M.en.plugins} not added β ${grund}`,
|