kimal3 0.4.0-durchstich.1 → 0.4.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 +2 -0
- package/claude-plugins/.claude-plugin/marketplace.json +8 -0
- package/claude-plugins/README.md +7 -0
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +6 -0
- package/claude-plugins/kimal3-logo/hooks/hooks.json +1 -0
- package/claude-plugins/kimal3-logo/hooks/register.tsx +80 -0
- package/coach/de/kimal3-coach.md +167 -0
- package/coach/en/kimal3-coach.md +167 -0
- package/dist/arbeitsplatz-dateien.js +1 -1
- package/dist/arbeitsplatz-finden.js +115 -0
- package/dist/arbeitsplatz-suche.js +176 -0
- package/dist/befehle/einrichten.js +311 -32
- package/dist/befehle/kurs.js +77 -0
- package/dist/befehle/login.js +8 -1
- package/dist/befehle/skill-install.js +16 -2
- package/dist/befehle-katalog-en.js +12 -0
- package/dist/befehle-katalog.js +12 -0
- package/dist/befehlswoerter.js +4 -0
- package/dist/coach-ablegen.js +130 -0
- package/dist/coach-rechnung.js +40 -0
- package/dist/einstellungen-mischen.js +68 -28
- package/dist/einstellungen-schreiben.js +48 -12
- package/dist/kimal3.js +6 -3
- package/dist/logo-ablegen.js +117 -0
- package/dist/plugins-mischen.js +78 -0
- package/dist/texte/coach.js +27 -0
- package/dist/texte/einrichten-marken.js +2 -2
- package/dist/texte/einrichten.js +73 -2
- package/dist/texte/kurs.js +21 -0
- package/dist/wache-schalter.js +30 -5
- package/package.json +29 -27
- package/skill/SKILL.md +7 -3
- package/skill/commands/k3feierabend.md +2 -3
- package/skill/commands/k3kurs.md +41 -53
- package/skill/commands/k3start.md +4 -3
- package/skill/en/SKILL.md +7 -3
- package/skill/en/commands/k3course.md +41 -54
- package/skill/en/commands/k3start.md +4 -3
- package/skill/en/commands/k3wrapup.md +3 -4
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { gemerktFuer, fundAus } from "./arbeitsplatz-finden.js";
|
|
6
|
+
import { werkbankSpracheAus } from "./werkbank-name.js";
|
|
7
|
+
/**
|
|
8
|
+
* Das Suchen auf der Platte (Spec 2026-10-16-vorhandener-arbeitsplatz-lagen.md, Bauform 2 und 3).
|
|
9
|
+
* Was aus dem Gefundenen folgt, rechnet `arbeitsplatz-finden.ts` -- ohne Dateisystem.
|
|
10
|
+
*
|
|
11
|
+
* 🔬 **Die Messung vom 16.10.** (auf Dennis' Rechner): Bis Ebene 3 unter dem Benutzerordner sind es
|
|
12
|
+
* 110 Ordner, gelesen in 0,2 s -- die Suche ist billig, solange sie Verknuepfungen meidet. Im
|
|
13
|
+
* Benutzerordner liegen elf davon (`OneDrive`, `Eigene Dateien`, `Anwendungsdaten` ...); Node
|
|
14
|
+
* meldet sie als Verknuepfung (`isSymbolicLink`), und wer sie betritt, laeuft im Kreis oder in
|
|
15
|
+
* fremde Konten. "Versteckt" ist unter Windows ein Dateiattribut, das Node nicht liefert --
|
|
16
|
+
* deshalb zaehlt nur der Punkt am Namensanfang.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ Die Zeitgrenze macht aus einer langsamen Platte ein "nicht vollstaendig", nie einen Abbruch:
|
|
19
|
+
* wer sicher sein will, dass nichts uebersehen wurde, fragt `vollstaendig`.
|
|
20
|
+
*/
|
|
21
|
+
/** Ordner, in denen nie ein Arbeitsplatz liegt, aber viele Dateien (kleingeschrieben verglichen). */
|
|
22
|
+
const AUSLASSEN = ["appdata", "library", "node_modules"]; // kein-anzeigetext: Ordnernamen
|
|
23
|
+
/** Nur auf macOS und nur direkt im Benutzerordner: grosse Ablagen ohne Arbeitsplatz. */
|
|
24
|
+
const AUSLASSEN_MAC_EBENE_1 = ["desktop", "downloads"]; // kein-anzeigetext: Ordnernamen
|
|
25
|
+
const TIEFE_HEIM = 3;
|
|
26
|
+
const TIEFE_DOKUMENTE = 2;
|
|
27
|
+
function namenIn(pfad, nurDateien) {
|
|
28
|
+
try {
|
|
29
|
+
return readdirSync(pfad, { withFileTypes: true }).filter((e) => !nurDateien || !e.isDirectory()).map((e) => e.name);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return []; // ein Ordner, in den ich nicht hineinsehe, zaehlt wie einer ohne unsere Dateien
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function istDatei(pfad) {
|
|
36
|
+
try {
|
|
37
|
+
return statSync(pfad).isFile();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Was dieser Ordner ueber sich verraet: `kontext/`, `context/`, `CLAUDE.md` und die Werkbank-Unterordner. */
|
|
44
|
+
export function kandidatAus(pfad) {
|
|
45
|
+
const eintraege = (() => {
|
|
46
|
+
try {
|
|
47
|
+
return readdirSync(pfad, { withFileTypes: true });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
return {
|
|
54
|
+
pfad,
|
|
55
|
+
kontextNamen: namenIn(join(pfad, "kontext"), true), // kein-anzeigetext: Ordnername
|
|
56
|
+
contextNamen: namenIn(join(pfad, "context"), true), // kein-anzeigetext: Ordnername
|
|
57
|
+
hatClaude: istDatei(join(pfad, "CLAUDE.md")), // kein-anzeigetext: Dateiname
|
|
58
|
+
werkbaenke: eintraege.filter((e) => e.isDirectory() && werkbankSpracheAus(e.name) !== null).map((e) => e.name),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Macht dieser Lesefehler die Suche unvollstaendig? Nur unter macOS, nur `EPERM`, nur auf `~/Documents`
|
|
63
|
+
* (V-6, V-8). Der Ordner wird ueber seinen Pfad erkannt -- er liegt auf Ebene 2 des Benutzerordners.
|
|
64
|
+
*/
|
|
65
|
+
export function macVerweigert(plattform, heim, ordner, code) {
|
|
66
|
+
return plattform === "darwin" && code === "EPERM" && ordner === join(heim, "Documents"); // kein-anzeigetext: Ordnername
|
|
67
|
+
}
|
|
68
|
+
export function sucheArbeitsplatz(a) {
|
|
69
|
+
const ende = Date.now() + a.fristMs;
|
|
70
|
+
const treffer = [];
|
|
71
|
+
let vollstaendig = true;
|
|
72
|
+
const pruefe = (pfad) => {
|
|
73
|
+
const k = kandidatAus(pfad);
|
|
74
|
+
// Nur mit echter Werkbank (Ruling 8): Ein Claude-Projekt des Kunden ist kein Treffer und wird weiter durchsucht.
|
|
75
|
+
if (fundAus([k], { mitWerkbank: true }).art === "keiner")
|
|
76
|
+
return false;
|
|
77
|
+
treffer.push(k);
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
// (a) Vom aktuellen Ordner aufwaerts -- billig und ohne Zeitgrenze. 200 wie in `findeArbeitsplatz`.
|
|
81
|
+
let oben = resolve(a.start);
|
|
82
|
+
for (let i = 0; i < 200; i++) {
|
|
83
|
+
pruefe(oben);
|
|
84
|
+
const eltern = dirname(oben);
|
|
85
|
+
if (eltern === oben)
|
|
86
|
+
break;
|
|
87
|
+
oben = eltern;
|
|
88
|
+
}
|
|
89
|
+
// (b) und (c): Die Tiefe zaehlt ab der Wurzel (Kinder = Ebene 1). Ein Treffer wird nicht weiter durchstiegen.
|
|
90
|
+
const durchsuche = (ordner, ebene, maxTiefe) => {
|
|
91
|
+
if (Date.now() >= ende) {
|
|
92
|
+
vollstaendig = false;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let eintraege;
|
|
96
|
+
try {
|
|
97
|
+
eintraege = readdirSync(ordner, { withFileTypes: true });
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
// Unter macOS fragt das System beim ersten Zugriff auf Dokumente nach; ein Nein ist ein `EPERM`
|
|
101
|
+
// genau dort -- das ist eine Luecke, die die Suche melden muss (V-6, V-8). Sonst: still weiter.
|
|
102
|
+
const code = e.code;
|
|
103
|
+
if (macVerweigert(a.plattform, a.heim, ordner, code))
|
|
104
|
+
vollstaendig = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const eintrag of eintraege) {
|
|
108
|
+
// Verknuepfungen nie betreten: `isDirectory()` ist fuer sie falsch, die Zeile sagt es trotzdem laut.
|
|
109
|
+
if (eintrag.isSymbolicLink() || !eintrag.isDirectory())
|
|
110
|
+
continue;
|
|
111
|
+
const name = eintrag.name.toLowerCase();
|
|
112
|
+
if (name.startsWith(".") || AUSLASSEN.includes(name))
|
|
113
|
+
continue;
|
|
114
|
+
if (a.plattform === "darwin" && ordner === a.heim && AUSLASSEN_MAC_EBENE_1.includes(name))
|
|
115
|
+
continue;
|
|
116
|
+
const kind = join(ordner, eintrag.name);
|
|
117
|
+
if (pruefe(kind))
|
|
118
|
+
continue;
|
|
119
|
+
if (ebene < maxTiefe)
|
|
120
|
+
durchsuche(kind, ebene + 1, maxTiefe);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
durchsuche(a.heim, 1, TIEFE_HEIM);
|
|
124
|
+
if (a.dokumente !== null)
|
|
125
|
+
durchsuche(a.dokumente, 1, TIEFE_DOKUMENTE);
|
|
126
|
+
return { fund: fundAus(treffer, { mitWerkbank: true }), vollstaendig };
|
|
127
|
+
}
|
|
128
|
+
/** Aus der Zeile `Personal REG_EXPAND_SZ %USERPROFILE%\OneDrive\Dokumente` den Pfad; `%NAME%` kommt aus `env`. */
|
|
129
|
+
export function dokumenteAusRegistry(ausgabe, env) {
|
|
130
|
+
const wert = /Personal\s+REG_\w+\s+(.+?)\s*$/m.exec(ausgabe)?.[1]; // kein-anzeigetext: Registry-Wert
|
|
131
|
+
if (wert === undefined)
|
|
132
|
+
return null;
|
|
133
|
+
// Unbekannte Namen bleiben stehen -- dann gibt es den Ordner nicht, und die Suche ueberspringt ihn.
|
|
134
|
+
return wert.replace(/%([^%]+)%/g, (rest, name) => env[name] ?? rest);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Der Ordner "Dokumente" unter Windows -- bei OneDrive-Konten liegt er NICHT unter dem Benutzerordner.
|
|
138
|
+
* `KIMAL3_DOKUMENTE` hat Vorrang (leer = keiner), damit Messungen nie den echten Ordner lesen.
|
|
139
|
+
*/
|
|
140
|
+
export function windowsDokumente() {
|
|
141
|
+
const vorgabe = process.env.KIMAL3_DOKUMENTE; // kein-anzeigetext: Umgebungsvariable
|
|
142
|
+
if (vorgabe !== undefined)
|
|
143
|
+
return vorgabe === "" ? null : vorgabe;
|
|
144
|
+
if (platform() !== "win32")
|
|
145
|
+
return null;
|
|
146
|
+
const r = spawnSync("reg", // kein-anzeigetext: Windows-Programm
|
|
147
|
+
["query", String.raw `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders`, "/v", "Personal"], // kein-anzeigetext: Registry
|
|
148
|
+
{ encoding: "utf8", windowsHide: true });
|
|
149
|
+
if (r.status !== 0)
|
|
150
|
+
return null;
|
|
151
|
+
return dokumenteAusRegistry(r.stdout, process.env);
|
|
152
|
+
}
|
|
153
|
+
const ORDNER = join(homedir(), ".kimal3");
|
|
154
|
+
const DATEI = join(ORDNER, "arbeitsplatz.json"); // kein-anzeigetext: Dateiname
|
|
155
|
+
/** Der gemerkte Ort dieses Kontos; kaputte oder fremde Datei -> `null`. Wirft nie. */
|
|
156
|
+
export function leseGemerktenOrt(email) {
|
|
157
|
+
try {
|
|
158
|
+
return gemerktFuer(JSON.parse(readFileSync(DATEI, "utf8")), email);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Den Ort merken. 🔴 Wirft nie: Ein nicht schreibbarer Merkzettel darf `einrichten` nicht scheitern
|
|
166
|
+
* lassen -- schlimmstenfalls wird einmal mehr gesucht. `mode: 0o700` wie in `gesehen.ts`.
|
|
167
|
+
*/
|
|
168
|
+
export function merkeOrt(o) {
|
|
169
|
+
try {
|
|
170
|
+
mkdirSync(ORDNER, { recursive: true, mode: 0o700 });
|
|
171
|
+
writeFileSync(DATEI, JSON.stringify(o, null, 2), "utf8");
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* nicht schreibbar -- der Befehl laeuft trotzdem durch */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import { leseAnmeldung } from "../anmeldung.js";
|
|
6
|
+
import { alterKursCoachAblage, alterKursCoachPfad, coachDateiLiegt, legeCoachAb, meldeCoach } from "../coach-ablegen.js";
|
|
7
|
+
import { istAlterKursCoach } from "../coach-rechnung.js";
|
|
8
|
+
import { tragePluginsEin } from "../einstellungen-schreiben.js";
|
|
9
|
+
import { logoAblegen } from "../logo-ablegen.js";
|
|
10
|
+
import { pluginStand } from "../plugins-mischen.js";
|
|
5
11
|
import { ruf } from "../api.js";
|
|
6
12
|
import { FIRMEN_OPTIONEN, fehlendeDateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
7
|
-
import {
|
|
13
|
+
import { WERKBANK_VORDRUCKE, ersteSitzungOffen, fundAus, werkbankWahl } from "../arbeitsplatz-finden.js";
|
|
14
|
+
import { kandidatAus, leseGemerktenOrt, merkeOrt, sucheArbeitsplatz, windowsDokumente } from "../arbeitsplatz-suche.js";
|
|
15
|
+
import { alsWort, zerlege } from "../argumente.js";
|
|
16
|
+
import { befehlIn, schalterIn } from "../befehlswoerter.js";
|
|
17
|
+
import { ORDNER_NAMEN } from "../kontext-stand.js";
|
|
8
18
|
import { sprache } from "../sprache.js";
|
|
9
19
|
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
20
|
+
import { COACH_TEXTE } from "../texte/coach.js";
|
|
10
21
|
import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
|
|
11
22
|
import { anmeldenMitBrowser } from "./login.js";
|
|
12
|
-
import {
|
|
23
|
+
import { SKILL_TEXTE } from "../texte/skill.js";
|
|
24
|
+
import { skillInstall, ursacheDes } from "./skill-install.js";
|
|
13
25
|
/**
|
|
14
26
|
* `kimal3 einrichten` / `kimal3 setup` -- der eine Befehl, den Claude beim Einstieg mit Leitung
|
|
15
27
|
* aufruft (Spec 2026-10-11, Abschnitt 0, S-1, S-2, S-7). Er verbindet, was es schon gibt:
|
|
16
28
|
* Anmeldung (mit Browser), die Vorlage von `/api/cli/arbeitsplatz`, `skill install`, und setzt
|
|
17
29
|
* die Kurs-Haken.
|
|
18
30
|
*
|
|
19
|
-
* 🔑
|
|
20
|
-
*
|
|
21
|
-
*
|
|
31
|
+
* 🔑 Einen VORHANDENEN Arbeitsplatz benutzt er, wie er ist (Spec 2026-10-16, Bauform 1): gemerkt in
|
|
32
|
+
* `~/.kimal3/arbeitsplatz.json`, sonst gesucht. Neu angelegt wird er im Benutzerordner (N-8), nie im
|
|
33
|
+
* Ordner, in dem Claude gerade steht -- die Sitzung ohne Ordner ist ein Wegwerf-Ordner der App (M-L2).
|
|
34
|
+
* 🔑 Nichts wird überschrieben: Nachgeliefert wird nur in einen eigenen, abgebrochenen Lauf (gemerkt
|
|
35
|
+
* `angefangen`, V-17). Ein fertiger Arbeitsplatz bekommt keine Datei zurück, die der Mensch gelöscht hat (V-16).
|
|
36
|
+
* 🔑 Jede Rückfrage (welcher Arbeitsplatz, welche Werkbank, fremder Ordner) endet mit `1` und fertigen
|
|
37
|
+
* Befehlen, BEVOR Plugins, Befehle oder Haken eingetragen werden -- Claude fragt den Menschen und startet
|
|
38
|
+
* den Befehl, der zu seiner Antwort passt.
|
|
22
39
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
40
|
+
* 🔑 superpowers und das Logo trägt er seit Bauplan 2026-10-15 selbst in Claudes Einstellungen ein
|
|
41
|
+
* (`pluginsEinrichten`) -- Claude installiert dafür nichts mehr per Befehlszeile oder Menü.
|
|
42
|
+
*
|
|
43
|
+
* 🔑 Den Coach legt er in Claudes Regel-Ordner (`~/.claude/rules/kimal3-coach.md`, Spec 2026-10-16, zweiter Teil von
|
|
44
|
+
* S-8) -- vorher fragt er den Server einmal nach dem Kursstand: Das frischt die Kontosprache auf und prüft die
|
|
45
|
+
* Anmeldung, damit Plugins, Befehle und Coach in der richtigen Sprache entstehen (Bauform 5). `~/.claude/CLAUDE.md`
|
|
46
|
+
* liest er nur: Steht dort der Coach aus dem alten Mini-Kurs, sagt eine Zeile es, und Claude fragt den Menschen (S-14).
|
|
25
47
|
*/
|
|
26
48
|
export async function einrichten(argv) {
|
|
27
49
|
const s = sprache();
|
|
@@ -29,25 +51,227 @@ export async function einrichten(argv) {
|
|
|
29
51
|
console.log(EINRICHTEN_TEXTE[s].help);
|
|
30
52
|
return 0;
|
|
31
53
|
}
|
|
32
|
-
const zerlegt = zerlege(argv, FIRMEN_OPTIONEN, s);
|
|
54
|
+
const zerlegt = zerlege(argv, [...FIRMEN_OPTIONEN, "arbeitsplatz", "werkbank"], s);
|
|
33
55
|
if (!zerlegt.ok) {
|
|
34
56
|
console.error(`${zerlegt.meldung}\n${EINRICHTEN_TEXTE[s].help}`);
|
|
35
57
|
return 1;
|
|
36
58
|
}
|
|
37
|
-
const
|
|
59
|
+
const angaben = zerlegt.werte.optionen;
|
|
60
|
+
const wunsch = { firma: angaben.firma, firmaId: angaben["firma-id"] };
|
|
61
|
+
// Ein mitkopierter Platzhalter aus einer Rückfrage („<pfad>“) ist kein Ordner -- sonst entstünde ein Ordner
|
|
62
|
+
// dieses Namens (macOS) oder ein Schreibfehler mitten im Lauf (Windows). Vor der Anmeldung: nichts geschrieben.
|
|
63
|
+
if (angaben.arbeitsplatz !== undefined && /[<>]/.test(angaben.arbeitsplatz))
|
|
64
|
+
return frage([EINRICHTEN_TEXTE[s].placeholderPath(angaben.arbeitsplatz)]);
|
|
38
65
|
console.log(EINRICHTEN_TEXTE[s].start);
|
|
39
66
|
const gespeichert = leseAnmeldung();
|
|
40
67
|
if (gespeichert)
|
|
41
68
|
console.log(ALLGEMEIN_TEXTE[s].signedInAs(gespeichert.email));
|
|
42
69
|
else {
|
|
43
|
-
const angemeldet = await
|
|
70
|
+
const angemeldet = await anmeldenEinmal();
|
|
44
71
|
if (angemeldet !== 0)
|
|
45
72
|
return angemeldet;
|
|
46
73
|
}
|
|
74
|
+
// Gleich nach der Anmeldung, VOR der ersten Zeile über den Arbeitsplatz: Erst danach kennt `sprache()` die Kontosprache,
|
|
75
|
+
// und schon „Dein Arbeitsplatz liegt schon unter …“ soll in ihr stehen (Gesamt-Durchsicht, Fund 7). Liest nur -- eine
|
|
76
|
+
// Rückfrage danach hat weiter nichts geschrieben (eine nötige Neuanmeldung ausgenommen, wie oben).
|
|
77
|
+
await kursstandVorab();
|
|
78
|
+
// Die Adresse entscheidet, welcher gemerkte Ort und welche Werkbank diesem Konto gehören (V-12b).
|
|
79
|
+
const email = leseAnmeldung()?.email ?? "";
|
|
80
|
+
const gemerkt = leseGemerktenOrt(email);
|
|
81
|
+
const ziel = zielBestimmen(angaben.arbeitsplatz, gemerkt);
|
|
82
|
+
if (ziel.art === "rueckfrage")
|
|
83
|
+
return frage(ziel.zeilen);
|
|
84
|
+
let ort;
|
|
85
|
+
if (ziel.art === "vorhanden") {
|
|
86
|
+
// 🔑 Kein Abruf der Vorlage (V-19): Ein gefundener Arbeitsplatz braucht sie nicht, also auch keine Firmenfrage.
|
|
87
|
+
console.log(EINRICHTEN_TEXTE[sprache()].foundWorkplace(ziel.pfad));
|
|
88
|
+
const werkbank = werkbankBestimmen(ziel.pfad, email, angaben.werkbank, gemerkt);
|
|
89
|
+
if (typeof werkbank !== "string")
|
|
90
|
+
return frage(werkbank);
|
|
91
|
+
ort = { pfad: ziel.pfad, werkbank };
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const neu = await neuAnlegen(ziel.pfad, wunsch, email, gespeichert !== null);
|
|
95
|
+
if (typeof neu === "number")
|
|
96
|
+
return neu;
|
|
97
|
+
ort = neu;
|
|
98
|
+
}
|
|
99
|
+
// 🌐 Die Antwort der Vorlage kann die Kontosprache umgestellt haben -- ab hier in der neuen.
|
|
100
|
+
const t = EINRICHTEN_TEXTE[sprache()];
|
|
101
|
+
console.log(t.workplaceLine(ort.pfad));
|
|
102
|
+
console.log(t.workbenchLine(ort.werkbank));
|
|
103
|
+
console.log(t.nextLine(ersteSitzungOffen(liesOderNull(join(ort.pfad, ort.werkbank, "CLAUDE.md"))))); // kein-anzeigetext: Dateiname
|
|
104
|
+
merkeOrt({ email, pfad: ort.pfad, werkbank: ort.werkbank, zustand: "fertig" });
|
|
105
|
+
// Plugins VOR der Wache: So hält ihre Sicherungskopie Sabines unveränderte Einstellungen (QA-Fund 9).
|
|
106
|
+
const git = gitAntwortet();
|
|
107
|
+
pluginsEinrichten(git);
|
|
108
|
+
// Der Coach VOR `skillInstall()`: Das frischt einen vorhandenen Coach auf und findet ihn so schon aktuell -- still.
|
|
109
|
+
meldeCoach(legeCoachAb(sprache(), true), sprache());
|
|
110
|
+
const befehle = await skillInstall();
|
|
111
|
+
const alt = alterKursCoachPfad();
|
|
112
|
+
if (istAlterKursCoach(liesOderNull(alt) ?? ""))
|
|
113
|
+
console.log(COACH_TEXTE[sprache()].oldCourseCoach(alt, alterKursCoachAblage()));
|
|
114
|
+
// `beat2-coach` heißt „Coach und /k3-Befehle sind dauerhaft eingerichtet“ (E-9): Unter dem Namen liegt eine DATEI
|
|
115
|
+
// (auch eine fremde, E-5 -- ein Ordner ist kein Coach) und die Befehle stehen. Logo und Plugins zählen nicht.
|
|
116
|
+
const coachSteht = befehle === 0 && coachDateiLiegt();
|
|
117
|
+
const haken = git ? ["beat2-werkzeuge", "beat2-kimal3", "beat2-arbeitsplatz", "beat2-coach"] : ["beat2-kimal3", "beat2-arbeitsplatz", "beat2-coach"];
|
|
118
|
+
await kursAbhaken(haken.filter((id) => id !== "beat2-coach" || coachSteht));
|
|
119
|
+
if (befehle !== 0)
|
|
120
|
+
return befehle;
|
|
121
|
+
console.log(EINRICHTEN_TEXTE[sprache()].done);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Fragt den Kursstand, bevor etwas in einer Sprache geschrieben wird (Spec 2026-10-16, Bauform 5): Jede Antwort trägt
|
|
126
|
+
* die Kontosprache, und `ruf` übernimmt sie -- so ist die Einrichtung in der Sprache, die Sabine gerade in der App
|
|
127
|
+
* gewählt hat (C-6). Den Stand selbst braucht `einrichten` nicht.
|
|
128
|
+
* 🔑 `401` -> neu anmelden (`anmeldenEinmal`) und noch einmal fragen. Scheitert das, läuft die Einrichtung trotzdem
|
|
129
|
+
* weiter (Durchsicht Aufgabe 5, Fund 5): Plugins, Coach und Befehle brauchen kein Netz; nur die Haken fehlen dann, und
|
|
130
|
+
* `kursAbhaken` sagt es in einer Zeile. Ein zweites `401` nach der Anmeldung bleibt ohne weitere Anmeldung.
|
|
131
|
+
* 🔑 Jeder andere Ausgang (auch `404` einer App ohne diese Route, Netzfehler) zählt nicht: weiter, ohne Zeile (Spec 6).
|
|
132
|
+
*/
|
|
133
|
+
async function kursstandVorab() {
|
|
134
|
+
const stand = async () => {
|
|
135
|
+
try {
|
|
136
|
+
return (await ruf("/api/cli/kurs", { methode: "GET", mitAusweis: true })).status; // kein-anzeigetext: Pfad der Schnittstelle
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
if ((await stand()) === 401 && (await anmeldenEinmal()) === 0)
|
|
143
|
+
await stand();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Höchstens EINE Anmeldung mit Browser je Lauf (Durchsicht Aufgabe 5, Fund 5). Jede davon wartet bis zu zehn Minuten
|
|
147
|
+
* auf Sabines Bestätigung; scheiterte die erste (abgelaufen, kein Netz), soll ein zweiter Versuch sie nicht noch einmal
|
|
148
|
+
* zehn Minuten warten lassen. Jeder weitere Aufruf antwortet `2` („nicht angemeldet“), ohne Browser.
|
|
149
|
+
*/
|
|
150
|
+
let anmeldungVersucht = false;
|
|
151
|
+
async function anmeldenEinmal() {
|
|
152
|
+
if (anmeldungVersucht)
|
|
153
|
+
return 2;
|
|
154
|
+
anmeldungVersucht = true;
|
|
155
|
+
return anmeldenMitBrowser();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Die Reihenfolge aus Spec 2026-10-16, Bauform 2 und 3: die Angabe des Menschen, dann der gemerkte Ort
|
|
159
|
+
* dieses Kontos, dann die Suche. Liest nur, schreibt nichts und fragt kein Netz.
|
|
160
|
+
*/
|
|
161
|
+
function zielBestimmen(angabe, gemerkt) {
|
|
162
|
+
const t = EINRICHTEN_TEXTE[sprache()];
|
|
163
|
+
if (angabe !== undefined) {
|
|
164
|
+
const pfad = resolve(angabe);
|
|
165
|
+
// Auch ein Ordner IN einem Arbeitsplatz meint diesen -- nie einen zweiten darin anlegen. Ohne Werkbank zählt nur
|
|
166
|
+
// der genannte Ordner selbst (V-12c); ein Ordner DARÜBER ist gesucht, nicht genannt, und braucht wie bei der Suche
|
|
167
|
+
// eine Werkbank (Ruling 8) -- sonst wäre `--arbeitsplatz ~/Projects/app/KI-Arbeitsplatz` das Repo `app`.
|
|
168
|
+
for (let oben = pfad;; oben = dirname(oben)) {
|
|
169
|
+
if (istArbeitsplatz(oben, oben !== pfad))
|
|
170
|
+
return { art: "vorhanden", pfad: oben };
|
|
171
|
+
if (dirname(oben) === oben)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
if (istFremd(pfad))
|
|
175
|
+
return { art: "rueckfrage", zeilen: [t.foreignFolder(pfad), t.otherPlace, platzBefehl(t.pathPlaceholder)] };
|
|
176
|
+
return { art: "neu", pfad };
|
|
177
|
+
}
|
|
178
|
+
// Ein eigener, abgebrochener Lauf wird fertig geschrieben (V-17) -- der einzige Fall, in dem nachgeliefert wird.
|
|
179
|
+
if (gemerkt?.zustand === "angefangen" && existsSync(gemerkt.pfad))
|
|
180
|
+
return { art: "neu", pfad: gemerkt.pfad };
|
|
181
|
+
if (gemerkt?.zustand === "fertig" && istArbeitsplatz(gemerkt.pfad, false))
|
|
182
|
+
return { art: "vorhanden", pfad: gemerkt.pfad };
|
|
183
|
+
const { fund, vollstaendig } = sucheArbeitsplatz({
|
|
184
|
+
start: process.cwd(),
|
|
185
|
+
heim: homedir(),
|
|
186
|
+
dokumente: windowsDokumente(),
|
|
187
|
+
plattform: platform(),
|
|
188
|
+
fristMs: 3000,
|
|
189
|
+
});
|
|
190
|
+
if (fund.art === "einer")
|
|
191
|
+
return { art: "vorhanden", pfad: fund.pfad };
|
|
192
|
+
if (fund.art === "mehrere")
|
|
193
|
+
return { art: "rueckfrage", zeilen: [t.severalFound, ...fund.pfade.map((p) => platzBefehl(p))] };
|
|
194
|
+
// 🔑 Es gab einen, oder die Suche kam nicht überall hin: dann NICHT still einen zweiten anlegen (V-7, V-8).
|
|
195
|
+
const woIstEr = [t.existingWorkplace, platzBefehl(t.pathPlaceholder), t.newWorkplace, platzBefehl(join(homedir(), ORDNER_NAMEN[sprache()].wurzel))];
|
|
196
|
+
if (gemerkt !== null)
|
|
197
|
+
return { art: "rueckfrage", zeilen: [t.rememberedGone(gemerkt.pfad), ...woIstEr] };
|
|
198
|
+
if (!vollstaendig)
|
|
199
|
+
return { art: "rueckfrage", zeilen: [t.searchIncomplete, ...woIstEr] };
|
|
200
|
+
return { art: "neu", pfad: null };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Welche Werkbank im gefundenen Arbeitsplatz diesem Konto gehört (Bauform 5) -- ihr Name, oder die Zeilen
|
|
204
|
+
* der Rückfrage. 🔑 Die Rückfrage-Befehle nennen den Arbeitsplatz mit: Kam er aus `--arbeitsplatz` (nach
|
|
205
|
+
* „mehrere gefunden“), fände ein Lauf nur mit `--werkbank` wieder mehrere -- eine Frage im Kreis.
|
|
206
|
+
*/
|
|
207
|
+
function werkbankBestimmen(pfad, email, angabe, gemerkt) {
|
|
208
|
+
const t = EINRICHTEN_TEXTE[sprache()];
|
|
209
|
+
const echte = kandidatAus(pfad).werkbaenke.filter((n) => !WERKBANK_VORDRUCKE.includes(n));
|
|
210
|
+
const frueher = gemerkt !== null && resolve(gemerkt.pfad) === resolve(pfad) ? gemerkt.werkbank : null;
|
|
211
|
+
const wahl = werkbankWahl(echte, email, angabe ?? frueher);
|
|
212
|
+
const mit = (name) => platzBefehl(pfad, name);
|
|
213
|
+
const eigene = [t.ownWorkbench, ...v12c(pfad)];
|
|
214
|
+
// Eine genannte Werkbank, die es hier nicht gibt, wird nie still durch eine andere ersetzt (Bauform 6).
|
|
215
|
+
const passt = wahl.art === "genau" && (angabe === undefined || wahl.name === angabe);
|
|
216
|
+
if (passt)
|
|
217
|
+
return wahl.name;
|
|
218
|
+
if (wahl.art === "keine")
|
|
219
|
+
return [t.noWorkbenchInWorkplace(pfad), ...v12c(pfad)];
|
|
220
|
+
if (wahl.art === "bestaetigen" && angabe === undefined) {
|
|
221
|
+
const andere = echte.filter((n) => n !== wahl.name);
|
|
222
|
+
return [t.confirmWorkbench(wahl.name), t.yes, mit(wahl.name), ...(andere.length > 0 ? [t.noOther, ...andere.map((n) => mit(n))] : []), ...eigene];
|
|
223
|
+
}
|
|
224
|
+
return [t.noMatchingWorkbench(pfad), t.oneOfThese, ...echte.map((n) => mit(n)), ...eigene];
|
|
225
|
+
}
|
|
226
|
+
/** Der Weg aus V-12c: `werkbank neu` arbeitet im aktuellen Ordner, erst der zweite Lauf trägt Plugins und Haken ein. */
|
|
227
|
+
function v12c(pfad) {
|
|
228
|
+
const t = EINRICHTEN_TEXTE[sprache()];
|
|
229
|
+
return [t.cdLine(alsWort(pfad, true)), t.commandLine(befehlIn("werkbank neu", sprache())), platzBefehl(pfad)]; // kein-anzeigetext: Befehlsschluessel
|
|
230
|
+
}
|
|
231
|
+
/** `kimal3 einrichten --arbeitsplatz "…" [--werkbank …]` in der Sprache dieses Laufs, zum Kopieren. */
|
|
232
|
+
function platzBefehl(pfad, werkbank) {
|
|
233
|
+
const s = sprache();
|
|
234
|
+
const teile = [befehlIn("einrichten", s), `--${schalterIn("arbeitsplatz", s)}`, alsWort(pfad, true)]; // kein-anzeigetext: Befehlsschluessel
|
|
235
|
+
if (werkbank !== undefined)
|
|
236
|
+
teile.push(`--${schalterIn("werkbank", s)}`, alsWort(werkbank)); // kein-anzeigetext: Befehlsschluessel
|
|
237
|
+
return EINRICHTEN_TEXTE[s].commandLine(teile.join(" "));
|
|
238
|
+
}
|
|
239
|
+
function frage(zeilen) {
|
|
240
|
+
console.log(zeilen.join("\n"));
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
/** `mitWerkbank`: wie bei der Suche nur mit echter Werkbank (Ruling 8) -- für Ordner, die der Mensch nicht selbst genannt hat. */
|
|
244
|
+
const istArbeitsplatz = (pfad, mitWerkbank) => fundAus([kandidatAus(pfad)], { mitWerkbank }).art === "einer";
|
|
245
|
+
/**
|
|
246
|
+
* Ein Ordner, der da ist, kein Arbeitsplatz ist und etwas enthält -- dort legt `einrichten` nichts ab (V-11).
|
|
247
|
+
* Ohne Werkbank-Pflicht: Ein eigener Arbeitsplatz mit gelöschter Werkbank ist nicht fremd, `einrichten` ergänzt ihn.
|
|
248
|
+
*/
|
|
249
|
+
function istFremd(pfad) {
|
|
250
|
+
try {
|
|
251
|
+
return statSync(pfad).isDirectory() && readdirSync(pfad).length > 0 && !istArbeitsplatz(pfad, false);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function liesOderNull(pfad) {
|
|
258
|
+
try {
|
|
259
|
+
return readFileSync(pfad, "utf8");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Einen neuen Arbeitsplatz anlegen oder einen eigenen, abgebrochenen fertig schreiben -- wie bisher über
|
|
267
|
+
* die Vorlage. 🔑 Gemerkt wird `angefangen` VOR der ersten Datei: Bricht der Lauf ab, sähe der halbe Ordner
|
|
268
|
+
* sonst beim nächsten Mal wie ein fremder aus (V-17).
|
|
269
|
+
*/
|
|
270
|
+
async function neuAnlegen(pfad, wunsch, email, gespeichert) {
|
|
47
271
|
let holen = await vorlageHolen("einrichten", "", wunsch, null);
|
|
48
272
|
// Die gespeicherte Anmeldung galt nicht mehr (401 -> 2): einmal neu anmelden, dann noch einmal holen.
|
|
49
273
|
if (!holen.ok && holen.wert === 2 && gespeichert) {
|
|
50
|
-
const angemeldet = await
|
|
274
|
+
const angemeldet = await anmeldenEinmal();
|
|
51
275
|
if (angemeldet !== 0)
|
|
52
276
|
return angemeldet;
|
|
53
277
|
holen = await vorlageHolen("einrichten", "", wunsch, null);
|
|
@@ -57,7 +281,7 @@ export async function einrichten(argv) {
|
|
|
57
281
|
// 🌐 Die Antwort kann die Kontosprache umgestellt haben -- ab hier in der neuen.
|
|
58
282
|
const t = EINRICHTEN_TEXTE[sprache()];
|
|
59
283
|
const { wurzel, dateien } = holen.daten;
|
|
60
|
-
if (istUnsichererOrdnername(wurzel)) {
|
|
284
|
+
if (pfad === null && istUnsichererOrdnername(wurzel)) {
|
|
61
285
|
console.error(t.unsafeFolder(wurzel));
|
|
62
286
|
return 3;
|
|
63
287
|
}
|
|
@@ -66,35 +290,90 @@ export async function einrichten(argv) {
|
|
|
66
290
|
console.error(t.noWorkbank);
|
|
67
291
|
return 3;
|
|
68
292
|
}
|
|
69
|
-
const ziel = join(homedir(), wurzel);
|
|
293
|
+
const ziel = pfad ?? join(homedir(), wurzel);
|
|
294
|
+
if (pfad === null && istFremd(ziel))
|
|
295
|
+
return frage([t.foreignFolder(ziel), t.otherPlace, platzBefehl(t.pathPlaceholder)]);
|
|
296
|
+
merkeOrt({ email, pfad: ziel, werkbank, zustand: "angefangen" });
|
|
70
297
|
const geschrieben = fehlendeDateienSchreiben(dateien, ziel);
|
|
71
298
|
if (!geschrieben.ok)
|
|
72
299
|
return geschrieben.wert;
|
|
73
300
|
console.log(t.filesWritten(geschrieben.neu, geschrieben.schonDa));
|
|
74
|
-
|
|
75
|
-
console.log(t.workbenchLine(werkbank));
|
|
76
|
-
const befehle = await skillInstall();
|
|
77
|
-
await kursAbhaken(gitAntwortet() ? ["beat2-werkzeuge", "beat2-kimal3", "beat2-arbeitsplatz"] : ["beat2-kimal3", "beat2-arbeitsplatz"]);
|
|
78
|
-
if (befehle !== 0)
|
|
79
|
-
return befehle;
|
|
80
|
-
console.log(t.done);
|
|
81
|
-
return 0;
|
|
301
|
+
return { pfad: ziel, werkbank };
|
|
82
302
|
}
|
|
83
|
-
/**
|
|
303
|
+
/**
|
|
304
|
+
* superpowers und das Logo: Ordner ablegen, in Claudes Einstellungen eintragen, eine Zeile `Plugins:`.
|
|
305
|
+
* 🔑 Scheitert etwas, bricht die Einrichtung NICHT ab (Lage P-3): Arbeitsplatz, Befehle und Wache
|
|
306
|
+
* stehen ja -- die Zeile sagt, was fehlt, und Claude nimmt dann den Menü-Weg für superpowers.
|
|
307
|
+
* 🔑 Ohne Git trägt er superpowers NICHT ein (Durchsicht 3, Fund 5): Claude Code klont Anthropics Katalog mit Git und
|
|
308
|
+
* scheiterte sonst bei jedem Start. Eine Zeile sagt es; der nächste Lauf mit Git holt es nach (Mac-Weg ohne Adminrechte).
|
|
309
|
+
*/
|
|
310
|
+
function pluginsEinrichten(git) {
|
|
311
|
+
const s = sprache();
|
|
312
|
+
const t = EINRICHTEN_TEXTE[s];
|
|
313
|
+
const ablage = logoAblegen(s);
|
|
314
|
+
// Ohne Logo-Ordner kommt superpowers trotzdem (Durchsicht 2, Fund 1): eine Zeile zum Logo, dann weiter.
|
|
315
|
+
if (!ablage.ok)
|
|
316
|
+
console.log(t.logoNotWritten(ablage.detail));
|
|
317
|
+
if (ablage.ok && ablage.alterStand)
|
|
318
|
+
console.log(t.logoOldVersion);
|
|
319
|
+
// Schrägstriche vorwärts: So steht der Pfad in Claudes Einstellungen auf jedem System gleich (P-13).
|
|
320
|
+
const eintrag = tragePluginsEin(ablage.ok ? ablage.ordner.replace(/\\/g, "/") : null, git);
|
|
321
|
+
if (eintrag.art === "abgelehnt") {
|
|
322
|
+
console.log(t.pluginsNotWritten(eintrag.grund === "kein-gueltiges-json" ? t.settingsInvalid : t.settingsForeign));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (eintrag.art === "gescheitert") {
|
|
326
|
+
// Den Schritt in Sabines Sprache, nie das interne Wort (Durchsicht-Fund 3) -- derselbe Satz wie bei der Wache.
|
|
327
|
+
console.log(t.pluginsNotWritten(t.settingsFailed(ursacheDes(eintrag, SKILL_TEXTE[s]))));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const stand = pluginStand(eintrag.inhalt);
|
|
331
|
+
const namen = { superpowers: "superpowers", logo: t.logoName };
|
|
332
|
+
const mit = (wert) => ["superpowers", "logo"].filter((p) => stand[p] === wert).map((p) => namen[p]);
|
|
333
|
+
console.log(t.pluginsLine(mit("an"), mit("aus"), eintrag.art === "eingetragen"));
|
|
334
|
+
// Auch wenn superpowers schon von früher drinsteht: ohne Git lädt es nicht (Durchsicht 4, Fund 3).
|
|
335
|
+
if (!git && stand.superpowers !== "aus")
|
|
336
|
+
console.log(t.superpowersNeedsGit);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Ist Git da? Node.js ist es, sonst liefe dieses Programm nicht.
|
|
340
|
+
* 🔴 AUF DEM MAC NIE BLIND `git --version` (Durchsicht 3, Fund 1): Ohne Entwickler-Tools ist `/usr/bin/git` nur ein
|
|
341
|
+
* Platzhalter, der das Installationsfenster öffnet -- auf einem Standardkonto samt Frage nach einem Admin-Passwort,
|
|
342
|
+
* mitten in der Einrichtung. Dort zählt Git nur, wenn die Entwickler-Tools da sind (`xcode-select -p`) oder ein Git
|
|
343
|
+
* außerhalb von `/usr/bin` im Pfad liegt (Homebrew) -- und dann wird genau DAS gefragt, nicht das erste im Pfad.
|
|
344
|
+
* Dieselbe Regel steht in der Anleitung, Schritt 1.
|
|
345
|
+
*/
|
|
84
346
|
function gitAntwortet() {
|
|
85
|
-
|
|
347
|
+
const antwortet = (befehl) => spawnSync(befehl, ["--version"], { stdio: "ignore", windowsHide: true }).status === 0;
|
|
348
|
+
if (platform() !== "darwin")
|
|
349
|
+
return antwortet("git");
|
|
350
|
+
if (spawnSync("xcode-select", ["-p"], { stdio: "ignore" }).status === 0)
|
|
351
|
+
return antwortet("git");
|
|
352
|
+
// Nur absolute Ordner: Ein leerer Eintrag oder `.` ergäbe den bloßen Namen `git`, und der fände wieder den
|
|
353
|
+
// Platzhalter in `/usr/bin` (Durchsicht 4, Fund 2).
|
|
354
|
+
const eigenes = (process.env.PATH ?? "").split(":").filter((ordner) => ordner.startsWith("/") && ordner !== "/usr/bin")
|
|
355
|
+
.map((ordner) => join(ordner, "git")).find((datei) => existsSync(datei));
|
|
356
|
+
return eigenes !== undefined && antwortet(eigenes);
|
|
86
357
|
}
|
|
87
358
|
/**
|
|
88
359
|
* Die Haken sind eine Zugabe: Scheitern sie, ist der Arbeitsplatz trotzdem da -- eine Zeile,
|
|
89
360
|
* kein Abbruch, und der nächste Lauf setzt sie (die Route ist dafür wiederholbar).
|
|
361
|
+
* 🔑 `401` -> einmal neu anmelden (S-8): Ohne diesen Zweig bliebe eine abgelaufene Anmeldung für immer stehen, und
|
|
362
|
+
* „der nächste Lauf holt das nach“ wäre falsch. Hat dieser Lauf schon eine Anmeldung versucht (beim Kursstand vorab),
|
|
363
|
+
* öffnet `anmeldenEinmal` keine zweite -- die Zeile sagt dann, dass die Haken fehlen.
|
|
90
364
|
*/
|
|
91
365
|
async function kursAbhaken(schritte) {
|
|
366
|
+
const haken = async () => {
|
|
367
|
+
try {
|
|
368
|
+
return (await ruf("/api/cli/kurs", { koerper: { schritte }, mitAusweis: true })).status; // kein-anzeigetext: Pfad der Schnittstelle
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
let status = await haken();
|
|
375
|
+
if (status === 401 && (await anmeldenEinmal()) === 0)
|
|
376
|
+
status = await haken();
|
|
92
377
|
const t = EINRICHTEN_TEXTE[sprache()];
|
|
93
|
-
|
|
94
|
-
const a = await ruf("/api/cli/kurs", { koerper: { schritte }, mitAusweis: true }); // kein-anzeigetext: Pfad der Schnittstelle
|
|
95
|
-
console.log(a.status === 200 ? t.courseMarked : t.courseNotMarked);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
console.log(t.courseNotMarked);
|
|
99
|
-
}
|
|
378
|
+
console.log(status === 200 ? t.courseMarked : t.courseNotMarked);
|
|
100
379
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { feld, ruf } from "../api.js";
|
|
2
|
+
import { sprache } from "../sprache.js";
|
|
3
|
+
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
4
|
+
import { KURS_TEXTE } from "../texte/kurs.js";
|
|
5
|
+
import { schlussFehler } from "../fehler.js";
|
|
6
|
+
import { BASIS_URL } from "../anmeldung.js";
|
|
7
|
+
const istZahl = (w) => typeof w === "number" && Number.isFinite(w);
|
|
8
|
+
function schrittAus(w) {
|
|
9
|
+
if (!w || typeof w !== "object")
|
|
10
|
+
return null;
|
|
11
|
+
const s = w;
|
|
12
|
+
if (typeof s.id !== "string" || !istZahl(s.beat) || typeof s.text !== "string" || (s.wer !== "claude" && s.wer !== "mensch"))
|
|
13
|
+
return null;
|
|
14
|
+
return { id: s.id, beat: s.beat, text: s.text, wer: s.wer };
|
|
15
|
+
}
|
|
16
|
+
/** Die Antwort von `GET /api/cli/kurs` -- `null`, wenn sie nicht die erwartete Form hat (z. B. ein WLAN-Anmeldeportal mit `200`). */
|
|
17
|
+
function standAus(koerper) {
|
|
18
|
+
if (!koerper || typeof koerper !== "object")
|
|
19
|
+
return null;
|
|
20
|
+
const k = koerper;
|
|
21
|
+
if (!Array.isArray(k.beats) || !Array.isArray(k.offen) || typeof k.abgeschlossen !== "boolean")
|
|
22
|
+
return null;
|
|
23
|
+
const beats = k.beats.map((b) => {
|
|
24
|
+
const r = b && typeof b === "object" ? b : {};
|
|
25
|
+
return istZahl(r.nummer) && istZahl(r.erledigt) && istZahl(r.gesamt) && typeof r.fertig === "boolean"
|
|
26
|
+
? { nummer: r.nummer, erledigt: r.erledigt, gesamt: r.gesamt, fertig: r.fertig }
|
|
27
|
+
: null;
|
|
28
|
+
});
|
|
29
|
+
const offen = k.offen.map(schrittAus);
|
|
30
|
+
const naechster = k.naechster === null ? null : schrittAus(k.naechster);
|
|
31
|
+
if (beats.includes(null) || offen.includes(null) || (k.naechster !== null && !naechster))
|
|
32
|
+
return null;
|
|
33
|
+
return { beats: beats, offen: offen, naechster, abgeschlossen: k.abgeschlossen };
|
|
34
|
+
}
|
|
35
|
+
/** Reine Rechnung: die Zeilen, die der Mensch (und Claude) zu sehen bekommen. */
|
|
36
|
+
export function kursZeilen(stand, s, basisUrl) {
|
|
37
|
+
const t = KURS_TEXTE[s];
|
|
38
|
+
const browser = t.imBrowser(basisUrl);
|
|
39
|
+
if (stand.abgeschlossen)
|
|
40
|
+
return [t.alleErledigt(stand.beats.reduce((n, b) => n + b.gesamt, 0)), browser];
|
|
41
|
+
const beats = stand.beats.map((b) => (b.fertig ? t.beatFertig(b.nummer) : t.beatTeil(b.nummer, b.erledigt, b.gesamt)));
|
|
42
|
+
const zeilen = [t.uebersicht(beats.join(" · "))];
|
|
43
|
+
if (stand.naechster)
|
|
44
|
+
zeilen.push(t.naechster(stand.naechster.text, stand.naechster.wer === "claude"));
|
|
45
|
+
// Alles Offene ausser dem, was schon als „Als Nächstes“ dasteht -- über die Kennung: Zwei offene Schritte
|
|
46
|
+
// eines Beats können denselben Text tragen, und nur der eine ist der nächste.
|
|
47
|
+
const rest = stand.offen.filter((o) => o.id !== stand.naechster?.id);
|
|
48
|
+
if (rest.length > 0) {
|
|
49
|
+
zeilen.push(t.nochOffen);
|
|
50
|
+
rest.forEach((o) => zeilen.push(t.offenZeile(o.beat, o.text, o.wer === "claude")));
|
|
51
|
+
}
|
|
52
|
+
zeilen.push(browser);
|
|
53
|
+
return zeilen;
|
|
54
|
+
}
|
|
55
|
+
/** Wo der Mensch im Mini-Kurs steht -- die Grundlage von `/k3kurs`. */
|
|
56
|
+
export async function kurs() {
|
|
57
|
+
const antwort = await ruf("/api/cli/kurs", { methode: "GET", mitAusweis: true }); // kein-anzeigetext: HTTP-Methode
|
|
58
|
+
// 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
|
|
59
|
+
const s = sprache();
|
|
60
|
+
if (antwort.status === 200) {
|
|
61
|
+
const stand = standAus(antwort.koerper);
|
|
62
|
+
if (!stand) {
|
|
63
|
+
console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
|
|
64
|
+
return 3;
|
|
65
|
+
}
|
|
66
|
+
kursZeilen(stand, s, BASIS_URL).forEach((z) => console.log(z));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
|
|
70
|
+
if (antwort.status === 401) {
|
|
71
|
+
console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const schluss = schlussFehler(antwort.status, meldung, s);
|
|
75
|
+
console.error(schluss.text);
|
|
76
|
+
return schluss.wert;
|
|
77
|
+
}
|