kimal3 0.4.0-durchstich.3 → 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 -8
- package/claude-plugins/README.md +7 -7
- package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +6 -6
- package/claude-plugins/kimal3-logo/hooks/hooks.json +1 -1
- package/claude-plugins/kimal3-logo/hooks/register.tsx +80 -80
- 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 +249 -31
- package/dist/befehle/kurs.js +77 -0
- package/dist/befehle/login.js +8 -1
- package/dist/befehle/skill-install.js +12 -0
- 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/kimal3.js +4 -2
- package/dist/texte/coach.js +27 -0
- package/dist/texte/einrichten-marken.js +2 -2
- package/dist/texte/einrichten.js +48 -2
- package/dist/texte/kurs.js +21 -0
- package/package.json +2 -1
- 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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Die reinen Rechnungen zum Finden eines vorhandenen Arbeitsplatzes (Spec
|
|
3
|
+
* 2026-10-16-vorhandener-arbeitsplatz-lagen.md) -- ohne Dateisystem. Das Suchen auf der
|
|
4
|
+
* Platte kommt in eine eigene Datei; hier steht nur, was aus dem Gefundenen folgt.
|
|
5
|
+
*
|
|
6
|
+
* 🔑 **Warum die Werkbank am ORDNERNAMEN erkannt wird** (Spec, Bauform 5): Dennis' eigene
|
|
7
|
+
* Werkbank hat keine `CLAUDE.md`. Eine Erkennung "Unterordner mit eigener `CLAUDE.md`"
|
|
8
|
+
* (wie `werkbankAusDateien`) fande bei ihm gar keine -- ausgerechnet beim ersten Menschen,
|
|
9
|
+
* der den Befehl benutzt.
|
|
10
|
+
*
|
|
11
|
+
* 🔑 **Warum `bestaetigen` statt still zu waehlen:** Ein Name, der nur ueber seinen Anfang
|
|
12
|
+
* passt (`werkbank-sabine` zu sabine.mueller@...), passt genauso zu Sabine Schmidt. Zwei
|
|
13
|
+
* Sabines in einer Firma waren der Befund 10 der Pruefrunde 2. Deshalb fragt `einrichten`
|
|
14
|
+
* einmal und merkt sich die Antwort zum Konto; nur der volle Name gilt ohne Frage.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ Die Vordrucke aus der Vorlage (`werkbank-dein-name`) gehoeren niemandem: Sie sind nie
|
|
17
|
+
* waehlbar und machen aus einem Ordner keinen Arbeitsplatz (V-15, Repo-Klon in Suchtiefe).
|
|
18
|
+
*/
|
|
19
|
+
import { arbeitsplatzAus } from "./kontext-stand.js";
|
|
20
|
+
import { WERKBANK_PRAEFIXE, werkbankOrdnerAus, werkbankSpracheAus } from "./werkbank-name.js";
|
|
21
|
+
/** Die Werkbank-Namen, die die Vorlage selbst mitbringt. */
|
|
22
|
+
export const WERKBANK_VORDRUCKE = ["werkbank-dein-name", "workbench-your-name"];
|
|
23
|
+
const istVordruck = (name) => WERKBANK_VORDRUCKE.includes(name);
|
|
24
|
+
/** Eine Schreibweise fuer den Vergleich: Windows unterscheidet Gross/klein nicht, `\` und `/` sind dasselbe. */
|
|
25
|
+
const pfadSchluessel = (pfad) => pfad.replaceAll("\\", "/").toLowerCase();
|
|
26
|
+
/**
|
|
27
|
+
* Welche der Kandidaten Arbeitsplätze sind.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 **`mitWerkbank` ist Pflicht, kein optionaler Schalter** (Ruling 8): Die Regel von `kontext runter` allein
|
|
30
|
+
* (eine `CLAUDE.md` und `context/company.md`) ist die übliche Form JEDES Claude-Projekts. Ohne Werkbank hielte die
|
|
31
|
+
* SUCHE das eigene Repo des Kunden für einen Arbeitsplatz, und V-12c ließe Claude dort eine Werkbank anlegen.
|
|
32
|
+
* Deshalb zählt beim Suchen (`true`) nur ein Ordner mit mindestens einer echten Werkbank. Hat der Mensch den
|
|
33
|
+
* Ordner dagegen SELBST genannt (`--arbeitsplatz`, der gemerkte Ort) oder geht es darum, ob ein Ordner fremd ist
|
|
34
|
+
* (`false`), zählt auch einer ohne Werkbank -- sonst wäre V-12c unerreichbar, und ein eigener Arbeitsplatz mit
|
|
35
|
+
* gelöschter Werkbank hieße „fremd“.
|
|
36
|
+
*/
|
|
37
|
+
export function fundAus(kandidaten, o) {
|
|
38
|
+
const pfade = [];
|
|
39
|
+
const gesehen = new Set();
|
|
40
|
+
for (const k of kandidaten) {
|
|
41
|
+
if (arbeitsplatzAus([k]).art === "keiner")
|
|
42
|
+
continue;
|
|
43
|
+
// Nur Vordrucke = die Vorlage aus dem Repo (V-15) -- in keinem Fall ein Arbeitsplatz.
|
|
44
|
+
const echte = k.werkbaenke.filter((n) => !istVordruck(n));
|
|
45
|
+
if (k.werkbaenke.length > 0 && echte.length === 0)
|
|
46
|
+
continue;
|
|
47
|
+
if (o.mitWerkbank && echte.length === 0)
|
|
48
|
+
continue;
|
|
49
|
+
const schluessel = pfadSchluessel(k.pfad);
|
|
50
|
+
if (gesehen.has(schluessel))
|
|
51
|
+
continue;
|
|
52
|
+
gesehen.add(schluessel);
|
|
53
|
+
pfade.push(k.pfad);
|
|
54
|
+
}
|
|
55
|
+
if (pfade.length === 0)
|
|
56
|
+
return { art: "keiner" };
|
|
57
|
+
if (pfade.length === 1)
|
|
58
|
+
return { art: "einer", pfad: pfade[0] };
|
|
59
|
+
return { art: "mehrere", pfade };
|
|
60
|
+
}
|
|
61
|
+
/** Der Name ohne sein Praefix (`werkbank-sabine` -> `sabine`); `null`, wenn es keine Werkbank ist. */
|
|
62
|
+
function kernAus(name) {
|
|
63
|
+
const sprache = werkbankSpracheAus(name);
|
|
64
|
+
return sprache === null ? null : name.slice(WERKBANK_PRAEFIXE[sprache].length);
|
|
65
|
+
}
|
|
66
|
+
export function werkbankWahl(namen, email, gewaehlt) {
|
|
67
|
+
const echte = namen.filter((n) => !istVordruck(n) && kernAus(n) !== null);
|
|
68
|
+
if (echte.length === 0)
|
|
69
|
+
return { art: "keine" };
|
|
70
|
+
if (gewaehlt !== null && echte.includes(gewaehlt))
|
|
71
|
+
return { art: "genau", name: gewaehlt };
|
|
72
|
+
// Der erwartete Kern ist in beiden Sprachen derselbe; das Praefix ist beim Vergleich egal (V-13).
|
|
73
|
+
const erwartet = werkbankOrdnerAus(email, "de");
|
|
74
|
+
const erwarteterKern = erwartet === null ? null : kernAus(erwartet);
|
|
75
|
+
if (erwarteterKern === null)
|
|
76
|
+
return { art: "keine-passende", namen: echte };
|
|
77
|
+
const voll = echte.find((n) => kernAus(n) === erwarteterKern);
|
|
78
|
+
if (voll !== undefined)
|
|
79
|
+
return { art: "genau", name: voll };
|
|
80
|
+
const anfang = echte.filter((n) => erwarteterKern.startsWith(`${kernAus(n)}-`));
|
|
81
|
+
if (anfang.length === 1)
|
|
82
|
+
return { art: "bestaetigen", name: anfang[0] };
|
|
83
|
+
if (anfang.length > 1)
|
|
84
|
+
return { art: "mehrere", namen: anfang };
|
|
85
|
+
return { art: "keine-passende", namen: echte };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Steht die Anleitung fuer die erste Sitzung noch in der Werkbank-`CLAUDE.md`? Sie geht erst,
|
|
89
|
+
* wenn der Mensch sie ausgefuellt hat (Marke weg, kein ✏️ mehr). Ohne Datei: nein.
|
|
90
|
+
*
|
|
91
|
+
* Eine Zitatzeile (erstes Zeichen `>`) zaehlt nicht: Die alte Vorlage behaelt dort fuer immer den
|
|
92
|
+
* Hinweis „✏️ Muss nicht von Hand ausgefuellt werden …“, auch bei ausgefuelltem Profil -- sonst
|
|
93
|
+
* schickte `kimal3 einrichten` jeden Bestandskunden wieder in „Lass uns einrichten“.
|
|
94
|
+
*/
|
|
95
|
+
export function ersteSitzungOffen(werkbankClaudeMd) {
|
|
96
|
+
if (werkbankClaudeMd === null)
|
|
97
|
+
return false;
|
|
98
|
+
const ohneZitate = werkbankClaudeMd.split(/\r?\n/).filter((z) => !z.trimStart().startsWith(">")).join("\n");
|
|
99
|
+
return ohneZitate.includes("<!-- kimal3:erste-sitzung -->") || ohneZitate.includes("✏️"); // kein-anzeigetext: Marke in der Datei, kein Satz fuer Menschen
|
|
100
|
+
}
|
|
101
|
+
/** Der gemerkte Ort -- nur wenn die Datei die richtige Form hat UND zu diesem Konto gehoert (V-12b). */
|
|
102
|
+
export function gemerktFuer(roh, email) {
|
|
103
|
+
if (typeof roh !== "object" || roh === null)
|
|
104
|
+
return null;
|
|
105
|
+
const o = roh;
|
|
106
|
+
if (typeof o.email !== "string" || typeof o.pfad !== "string")
|
|
107
|
+
return null;
|
|
108
|
+
if (o.werkbank !== null && typeof o.werkbank !== "string")
|
|
109
|
+
return null;
|
|
110
|
+
if (o.zustand !== "angefangen" && o.zustand !== "fertig")
|
|
111
|
+
return null;
|
|
112
|
+
if (o.email.toLowerCase() !== email.toLowerCase())
|
|
113
|
+
return null;
|
|
114
|
+
return { email: o.email, pfad: o.pfad, werkbank: o.werkbank, zustand: o.zustand };
|
|
115
|
+
}
|
|
@@ -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,16 +1,23 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { leseAnmeldung } from "../anmeldung.js";
|
|
6
|
+
import { alterKursCoachAblage, alterKursCoachPfad, coachDateiLiegt, legeCoachAb, meldeCoach } from "../coach-ablegen.js";
|
|
7
|
+
import { istAlterKursCoach } from "../coach-rechnung.js";
|
|
6
8
|
import { tragePluginsEin } from "../einstellungen-schreiben.js";
|
|
7
9
|
import { logoAblegen } from "../logo-ablegen.js";
|
|
8
10
|
import { pluginStand } from "../plugins-mischen.js";
|
|
9
11
|
import { ruf } from "../api.js";
|
|
10
12
|
import { FIRMEN_OPTIONEN, fehlendeDateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
|
|
11
|
-
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";
|
|
12
18
|
import { sprache } from "../sprache.js";
|
|
13
19
|
import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
|
|
20
|
+
import { COACH_TEXTE } from "../texte/coach.js";
|
|
14
21
|
import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
|
|
15
22
|
import { anmeldenMitBrowser } from "./login.js";
|
|
16
23
|
import { SKILL_TEXTE } from "../texte/skill.js";
|
|
@@ -21,14 +28,22 @@ import { skillInstall, ursacheDes } from "./skill-install.js";
|
|
|
21
28
|
* Anmeldung (mit Browser), die Vorlage von `/api/cli/arbeitsplatz`, `skill install`, und setzt
|
|
22
29
|
* die Kurs-Haken.
|
|
23
30
|
*
|
|
24
|
-
* 🔑
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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.
|
|
27
39
|
*
|
|
28
40
|
* 🔑 superpowers und das Logo trägt er seit Bauplan 2026-10-15 selbst in Claudes Einstellungen ein
|
|
29
41
|
* (`pluginsEinrichten`) -- Claude installiert dafür nichts mehr per Befehlszeile oder Menü.
|
|
30
42
|
*
|
|
31
|
-
*
|
|
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).
|
|
32
47
|
*/
|
|
33
48
|
export async function einrichten(argv) {
|
|
34
49
|
const s = sprache();
|
|
@@ -36,25 +51,227 @@ export async function einrichten(argv) {
|
|
|
36
51
|
console.log(EINRICHTEN_TEXTE[s].help);
|
|
37
52
|
return 0;
|
|
38
53
|
}
|
|
39
|
-
const zerlegt = zerlege(argv, FIRMEN_OPTIONEN, s);
|
|
54
|
+
const zerlegt = zerlege(argv, [...FIRMEN_OPTIONEN, "arbeitsplatz", "werkbank"], s);
|
|
40
55
|
if (!zerlegt.ok) {
|
|
41
56
|
console.error(`${zerlegt.meldung}\n${EINRICHTEN_TEXTE[s].help}`);
|
|
42
57
|
return 1;
|
|
43
58
|
}
|
|
44
|
-
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)]);
|
|
45
65
|
console.log(EINRICHTEN_TEXTE[s].start);
|
|
46
66
|
const gespeichert = leseAnmeldung();
|
|
47
67
|
if (gespeichert)
|
|
48
68
|
console.log(ALLGEMEIN_TEXTE[s].signedInAs(gespeichert.email));
|
|
49
69
|
else {
|
|
50
|
-
const angemeldet = await
|
|
70
|
+
const angemeldet = await anmeldenEinmal();
|
|
51
71
|
if (angemeldet !== 0)
|
|
52
72
|
return angemeldet;
|
|
53
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) {
|
|
54
271
|
let holen = await vorlageHolen("einrichten", "", wunsch, null);
|
|
55
272
|
// Die gespeicherte Anmeldung galt nicht mehr (401 -> 2): einmal neu anmelden, dann noch einmal holen.
|
|
56
273
|
if (!holen.ok && holen.wert === 2 && gespeichert) {
|
|
57
|
-
const angemeldet = await
|
|
274
|
+
const angemeldet = await anmeldenEinmal();
|
|
58
275
|
if (angemeldet !== 0)
|
|
59
276
|
return angemeldet;
|
|
60
277
|
holen = await vorlageHolen("einrichten", "", wunsch, null);
|
|
@@ -64,7 +281,7 @@ export async function einrichten(argv) {
|
|
|
64
281
|
// 🌐 Die Antwort kann die Kontosprache umgestellt haben -- ab hier in der neuen.
|
|
65
282
|
const t = EINRICHTEN_TEXTE[sprache()];
|
|
66
283
|
const { wurzel, dateien } = holen.daten;
|
|
67
|
-
if (istUnsichererOrdnername(wurzel)) {
|
|
284
|
+
if (pfad === null && istUnsichererOrdnername(wurzel)) {
|
|
68
285
|
console.error(t.unsafeFolder(wurzel));
|
|
69
286
|
return 3;
|
|
70
287
|
}
|
|
@@ -73,22 +290,15 @@ export async function einrichten(argv) {
|
|
|
73
290
|
console.error(t.noWorkbank);
|
|
74
291
|
return 3;
|
|
75
292
|
}
|
|
76
|
-
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" });
|
|
77
297
|
const geschrieben = fehlendeDateienSchreiben(dateien, ziel);
|
|
78
298
|
if (!geschrieben.ok)
|
|
79
299
|
return geschrieben.wert;
|
|
80
300
|
console.log(t.filesWritten(geschrieben.neu, geschrieben.schonDa));
|
|
81
|
-
|
|
82
|
-
console.log(t.workbenchLine(werkbank));
|
|
83
|
-
// Plugins VOR der Wache: So hält ihre Sicherungskopie Sabines unveränderte Einstellungen (QA-Fund 9).
|
|
84
|
-
const git = gitAntwortet();
|
|
85
|
-
pluginsEinrichten(git);
|
|
86
|
-
const befehle = await skillInstall();
|
|
87
|
-
await kursAbhaken(git ? ["beat2-werkzeuge", "beat2-kimal3", "beat2-arbeitsplatz"] : ["beat2-kimal3", "beat2-arbeitsplatz"]);
|
|
88
|
-
if (befehle !== 0)
|
|
89
|
-
return befehle;
|
|
90
|
-
console.log(t.done);
|
|
91
|
-
return 0;
|
|
301
|
+
return { pfad: ziel, werkbank };
|
|
92
302
|
}
|
|
93
303
|
/**
|
|
94
304
|
* superpowers und das Logo: Ordner ablegen, in Claudes Einstellungen eintragen, eine Zeile `Plugins:`.
|
|
@@ -148,14 +358,22 @@ function gitAntwortet() {
|
|
|
148
358
|
/**
|
|
149
359
|
* Die Haken sind eine Zugabe: Scheitern sie, ist der Arbeitsplatz trotzdem da -- eine Zeile,
|
|
150
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.
|
|
151
364
|
*/
|
|
152
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();
|
|
153
377
|
const t = EINRICHTEN_TEXTE[sprache()];
|
|
154
|
-
|
|
155
|
-
const a = await ruf("/api/cli/kurs", { koerper: { schritte }, mitAusweis: true }); // kein-anzeigetext: Pfad der Schnittstelle
|
|
156
|
-
console.log(a.status === 200 ? t.courseMarked : t.courseNotMarked);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
console.log(t.courseNotMarked);
|
|
160
|
-
}
|
|
378
|
+
console.log(status === 200 ? t.courseMarked : t.courseNotMarked);
|
|
161
379
|
}
|