kimal3 0.4.0 → 0.6.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 CHANGED
@@ -42,8 +42,12 @@ kimal3 kontext fragen Die Fragen über deine Firma beantworten — Claude f
42
42
  kimal3 kontext hoch Deine lokalen Änderungen an den fünf Firmen-Texten hochschicken
43
43
  kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
44
44
  kimal3 projekt liste Deine eingetragenen Projekte anzeigen
45
+ kimal3 plan Planung hochladen oder Rückmeldungen holen
46
+ kimal3 plan hoch Die Planung aus plan/stand.json hochschicken (--probe: nur prüfen)
45
47
  kimal3 kurs Zeigen, wo du im Mini-Kurs stehst
46
48
  kimal3 firma daten Die Daten deiner Firma holen, damit Claude sie auswerten kann
49
+ kimal3 zeigen start Eine Aufnahme starten, während du eine Aufgabe vormachst
50
+ kimal3 zeigen status Nachsehen, ob gerade aufgenommen wird und ob der Aufnehmer eingerichtet ist
47
51
  kimal3 wache Läuft von selbst beim Start von Claude: prüft Zugang, Nachricht, Version, Skill
48
52
  kimal3 wache aus Die tägliche Prüfung abschalten — sie sagt dann nichts mehr
49
53
  kimal3 wache an Sie wieder einschalten
@@ -180,8 +184,12 @@ kimal3 context questions Answer the questions about your company — Claude tak
180
184
  kimal3 context upload Upload your local changes to the five company texts
181
185
  kimal3 project new … Add a project to your success profile
182
186
  kimal3 project list Show the projects you've added
187
+ kimal3 plan Upload planning or fetch responses
188
+ kimal3 plan upload Upload the planning in plan/stand.json (--dry-run: only check)
183
189
  kimal3 course Show where you are in the mini-course
184
190
  kimal3 company data Fetch your company's data so Claude can analyse it
191
+ kimal3 show start Start a recording while you show a task
192
+ kimal3 show status Check whether a recording is running and whether the recorder is set up
185
193
  kimal3 watch Runs by itself when Claude starts: checks your access, messages, version and skill
186
194
  kimal3 watch off Switch the daily check off — it then says nothing at all
187
195
  kimal3 watch on Switch it back on
@@ -118,8 +118,8 @@ nutze sie, statt vorschnell loszulegen.
118
118
  Beispiel Node.js: `winget install OpenJS.NodeJS.LTS` / `brew install node`. Fehlt Homebrew, frag vorher: Es
119
119
  einzurichten braucht einmal das Administrator-Passwort.
120
120
  **Ohne Adminrechte** (oft am Firmenrechner) geht das nicht. Dann kommt das Werkzeug nur für dieses Konto in den
121
- Benutzerordner, so wie `kimal3 einrichten` Node.js anlegt; sag dazu, dass es dann nicht mit `upgrade` mitkommt, und
122
- biete eine Nachricht an die IT an.
121
+ Benutzerordner, so wie die kimal3-Einrichtungsanleitung Node.js ohne Adminrechte nach `~/.kimal3/node` legt; sag dazu, dass es
122
+ dann nicht mit `upgrade` mitkommt, und biete eine Nachricht an die IT an.
123
123
  **Ist ein Werkzeug schon da, aktualisiere es auf dem Weg, auf dem es gekommen ist** — sonst liegen zwei Fassungen
124
124
  nebeneinander, und es ist unklar, welche startet.
125
125
  - **Aktualisieren nach der Versionsnummer** (`16.2.10`): **letzte Zahl** = nur Fehler und Sicherheitslücken, zügig
@@ -118,8 +118,8 @@ use them instead of starting off too quickly.
118
118
  Example Node.js: `winget install OpenJS.NodeJS.LTS` / `brew install node`. If Homebrew is missing, ask first:
119
119
  setting it up needs the administrator password once.
120
120
  **Without admin rights** (often on a company computer) this does not work. Then the tool goes into the user folder,
121
- for this account only, the way `kimal3 setup` sets up Node.js; say that it will then not come along with `upgrade`,
122
- and offer a message to IT.
121
+ for this account only, the way the kimal3 setup guide puts Node.js into `~/.kimal3/node` without admin rights; say that it
122
+ will then not come along with `upgrade`, and offer a message to IT.
123
123
  **If a tool is already there, update it the way it came** — otherwise two versions sit side by side, and it is
124
124
  unclear which one starts.
125
125
  - **Update by version number** (`16.2.10`): **last number** = only bugs and security holes, take it
@@ -314,7 +314,7 @@ function pluginsEinrichten(git) {
314
314
  // Ohne Logo-Ordner kommt superpowers trotzdem (Durchsicht 2, Fund 1): eine Zeile zum Logo, dann weiter.
315
315
  if (!ablage.ok)
316
316
  console.log(t.logoNotWritten(ablage.detail));
317
- if (ablage.ok && ablage.alterStand)
317
+ if (ablage.ok && ablage.stand === "alt")
318
318
  console.log(t.logoOldVersion);
319
319
  // Schrägstriche vorwärts: So steht der Pfad in Claudes Einstellungen auf jedem System gleich (P-13).
320
320
  const eintrag = tragePluginsEin(ablage.ok ? ablage.ordner.replace(/\\/g, "/") : null, git);
@@ -6,6 +6,7 @@ import { anmeldeAdresse, oeffneImBrowser } from "../browser.js";
6
6
  import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
7
7
  import { EINRICHTEN_TEXTE } from "../texte/einrichten.js";
8
8
  import { frischeCoachAuf } from "../coach-ablegen.js";
9
+ import { frischeLogoAuf } from "../logo-ablegen.js";
9
10
  import { stelleSkillUm } from "./skill-tausch.js";
10
11
  const warte = (ms) => new Promise((f) => setTimeout(f, ms));
11
12
  // 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
@@ -31,9 +32,12 @@ export async function login() {
31
32
  const code = await warteAufBestaetigung(start.geraeteCode);
32
33
  // Der Coach zieht mit der Kontosprache mit -- nur beim BEFEHL `kimal3 login`, nicht in der geteilten Warteschleife:
33
34
  // `einrichten` meldet anschließend seinen eigenen Coach, eine zweite Zeile wäre doppelt (Gesamt-Durchsicht, Fund 4;
34
- // dasselbe Muster wie `skillInstallBefehl`). Frischt nur auf, legt nie an (Spec C-13b).
35
- if (code === 0)
35
+ // dasselbe Muster wie `skillInstallBefehl`). Frischt nur auf, legt nie an (Spec C-13b). Das Logo genauso (Spec
36
+ // 2026-10-18, L-1, L-8): `einrichten` legt es nach der Anmeldung ohnehin selbst ab.
37
+ if (code === 0) {
36
38
  frischeCoachAuf(sprache());
39
+ frischeLogoAuf(sprache());
40
+ }
37
41
  return code;
38
42
  }
39
43
  /**
@@ -0,0 +1,114 @@
1
+ import { feld, ruf } from "../api.js";
2
+ import { schalterOhneWert, zerlege } from "../argumente.js";
3
+ import { sprache } from "../sprache.js";
4
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
5
+ import { PLAN_TEXTE } from "../texte/plan.js";
6
+ import { leseSpecTexte, schreibeNachHochladen, specKoepfeAus } from "../plan-datei.js";
7
+ import { planAbsage, planDateiOderAbsage } from "./plan.js";
8
+ const istZahl = (w) => typeof w === "number" && Number.isInteger(w) && w >= 0;
9
+ function hochgeladenAus(koerper) {
10
+ if (!koerper || typeof koerper !== "object")
11
+ return null;
12
+ const k = koerper;
13
+ if (k.probe === true)
14
+ return null;
15
+ if (typeof k.projektId !== "string" || typeof k.titel !== "string" || typeof k.adresse !== "string" || typeof k.hochgeladenAm !== "string")
16
+ return null;
17
+ if (!istZahl(k.stories) || !istZahl(k.deinTest))
18
+ return null;
19
+ return { projektId: k.projektId, titel: k.titel, stories: k.stories, deinTest: k.deinTest, adresse: k.adresse, hochgeladenAm: k.hochgeladenAm, specTexte: istZahl(k.specTexte) ? k.specTexte : null };
20
+ }
21
+ function probeAus(koerper) {
22
+ if (!koerper || typeof koerper !== "object")
23
+ return null;
24
+ const k = koerper;
25
+ return k.probe === true && istZahl(k.stories) && istZahl(k.uebernommen) ? { stories: k.stories, uebernommen: k.uebernommen, specTexte: istZahl(k.specTexte) ? k.specTexte : null } : null;
26
+ }
27
+ /**
28
+ * Die Zeile aus US-16. `angekommen` kommt aus der Antwort des Servers (Eintraege mit Text), nicht aus dem, was
29
+ * die CLI geschickt hat: Eine alte App ohne das Feld antwortet ohne Zahl, und dann steht hier lieber keine Zeile
30
+ * als eine, die ein Hochladen behauptet, das nicht stattgefunden hat.
31
+ */
32
+ function specTexteZeile(angekommen, gesamt, ohne, s) {
33
+ if (angekommen === null)
34
+ return null;
35
+ const t = PLAN_TEXTE[s];
36
+ return t.specTexte(angekommen, gesamt, ohne.map((o) => t.specTextOhneEintrag(o.titel, t.specTextOhneGrund(o.grund, o.pfad))));
37
+ }
38
+ export async function planHoch(argv) {
39
+ let s = sprache();
40
+ if (argv.includes("--help") || argv.includes("-h")) {
41
+ console.log(PLAN_TEXTE[s].uploadHelp);
42
+ return 0;
43
+ }
44
+ // ⚠️ `zerlege` kennt keine Schalter ohne Wert -- beide VOR der Zerlegung heraus, wie `--ueberschreiben` in kontext-hoch.ts.
45
+ // Was danach uebrig bleibt, ist ein Tippfehler: `zerlege` ohne erlaubte Optionen sagt dazu den passenden Satz.
46
+ const ueber = schalterOhneWert(argv, "ueberschreiben");
47
+ const probe = schalterOhneWert(ueber.rest, "probe");
48
+ const zerlegt = zerlege(probe.rest, [], s);
49
+ if (!zerlegt.ok) {
50
+ console.error(zerlegt.meldung);
51
+ return 1;
52
+ }
53
+ const datei = planDateiOderAbsage(s);
54
+ if (datei === null)
55
+ return 1;
56
+ const hatteNummer = typeof datei.json.projekt_id === "string";
57
+ // Der Text der Specs geht im selben Aufruf mit (US-16); pro Spec liegt entweder der Text oder der Grund vor.
58
+ const koepfe = specKoepfeAus(datei.json);
59
+ // Eine Funktion baut den Rumpf: an ihm misst `leseSpecTexte` die 4 MB, und genau er geht hinaus (US-16).
60
+ const rumpf = (specTexte) => ({ stand: datei.text, ueberschreiben: ueber.gesetzt, probe: probe.gesetzt, specTexte });
61
+ const texte = leseSpecTexte(process.cwd(), koepfe, rumpf);
62
+ const antwort = await ruf("/api/cli/projekt/plan", {
63
+ methode: "POST", // kein-anzeigetext: HTTP-Verb
64
+ mitAusweis: true,
65
+ koerper: rumpf(texte.eintraege),
66
+ });
67
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
68
+ s = sprache();
69
+ const t = PLAN_TEXTE[s];
70
+ if (antwort.status !== 200) {
71
+ const code = planAbsage(antwort, s);
72
+ // Ruling 10: `503 teilweise` -- gespeichert, aber die Rueckmeldungen noch nicht markiert. `basis` muss trotzdem
73
+ // nachgetragen werden, sonst lehnt der zweite Lauf als „veraltet" ab; `uebernommen` bleibt fuer genau diesen Lauf stehen.
74
+ // Fehlen Nummer oder Zeitpunkt in der Antwort, wird nichts geschrieben (geraten wird hier nie).
75
+ const projektId = feld(antwort.koerper, "projektId"); // kein-anzeigetext: Feldname der Antwort
76
+ const hochgeladenAm = feld(antwort.koerper, "hochgeladenAm"); // kein-anzeigetext: Feldname der Antwort
77
+ if (antwort.status === 503 && feld(antwort.koerper, "grund") === "teilweise" && projektId !== null && hochgeladenAm !== null &&
78
+ !schreibeNachHochladen(process.cwd(), projektId, hochgeladenAm, false)) {
79
+ console.error(t.writeBackFailed(projektId, hochgeladenAm, false));
80
+ }
81
+ return code;
82
+ }
83
+ if (probe.gesetzt) {
84
+ const p = probeAus(antwort.koerper);
85
+ if (p === null) {
86
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
87
+ return 3;
88
+ }
89
+ console.log(t.dryRun(p.stories, p.uebernommen));
90
+ const zeile = specTexteZeile(p.specTexte, koepfe.length, texte.ohne, s);
91
+ if (zeile !== null)
92
+ console.log(zeile);
93
+ return 0;
94
+ }
95
+ const h = hochgeladenAus(antwort.koerper);
96
+ if (h === null) {
97
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
98
+ return 3;
99
+ }
100
+ // Erst sagen, was oben angekommen ist, dann, was daneben schiefging (dieselbe Reihenfolge wie in kontext-hoch.ts).
101
+ console.log(t.uploaded(h.titel, h.stories, h.deinTest));
102
+ const zeile = specTexteZeile(h.specTexte, koepfe.length, texte.ohne, s);
103
+ if (zeile !== null)
104
+ console.log(zeile);
105
+ console.log(t.view(h.adresse));
106
+ if (!schreibeNachHochladen(process.cwd(), h.projektId, h.hochgeladenAm, true)) {
107
+ // `1`, nicht `0`: Ohne `basis` in der Datei lehnt das naechste Hochladen als „veraltet" ab -- hier ist noch etwas zu tun.
108
+ console.error(t.writeBackFailed(h.projektId, h.hochgeladenAm, true));
109
+ return 1;
110
+ }
111
+ if (!hatteNummer)
112
+ console.log(t.idNowInFile);
113
+ return 0;
114
+ }
@@ -0,0 +1,110 @@
1
+ import { feld, ruf } from "../api.js";
2
+ import { sprache } from "../sprache.js";
3
+ import { ALLGEMEIN_TEXTE } from "../texte/allgemein.js";
4
+ import { PLAN_TEXTE } from "../texte/plan.js";
5
+ import { schlussFehler } from "../fehler.js";
6
+ import { lesePlanDatei } from "../plan-datei.js";
7
+ const istZahl = (w) => typeof w === "number" && Number.isInteger(w);
8
+ const textOderNull = (w) => typeof w === "string" || w === null;
9
+ function offenAus(w) {
10
+ if (!w || typeof w !== "object")
11
+ return null;
12
+ const r = w;
13
+ if (!istZahl(r.nummer) || (r.art !== "passt_nicht" && r.art !== "thema") || typeof r.notiz !== "string")
14
+ return null;
15
+ if (!textOderNull(r.spec) || !textOderNull(r.story) || !(istZahl(r.runde) || r.runde === null))
16
+ return null;
17
+ // Ein „passt nicht" ohne Story und Runde waere eine Zeile, aus der Claude nichts machen kann.
18
+ if (r.art === "passt_nicht" && (r.spec === null || r.story === null || r.runde === null))
19
+ return null;
20
+ return { nummer: r.nummer, art: r.art, spec: r.spec, story: r.story, runde: r.runde, notiz: r.notiz };
21
+ }
22
+ function bestaetigtAus(w) {
23
+ if (!w || typeof w !== "object")
24
+ return null;
25
+ const r = w;
26
+ return typeof r.spec === "string" && typeof r.story === "string" && istZahl(r.runde) ? { spec: r.spec, story: r.story, runde: r.runde } : null;
27
+ }
28
+ /** `null`, wenn die Antwort nicht die erwartete Form hat (z. B. ein WLAN-Anmeldeportal mit `200`). */
29
+ export function planAntwortAus(koerper) {
30
+ if (!koerper || typeof koerper !== "object")
31
+ return null;
32
+ const k = koerper;
33
+ if (typeof k.adresse !== "string" || !Array.isArray(k.offen) || !Array.isArray(k.bestaetigt))
34
+ return null;
35
+ const offen = k.offen.map((o) => offenAus(o));
36
+ const bestaetigt = k.bestaetigt.map((b) => bestaetigtAus(b));
37
+ if (offen.includes(null) || bestaetigt.includes(null))
38
+ return null;
39
+ return { adresse: k.adresse, offen: offen, bestaetigt: bestaetigt };
40
+ }
41
+ /** Reine Rechnung: die Zeilen, die Claude zu sehen bekommt. */
42
+ export function planZeilen(a, s) {
43
+ const t = PLAN_TEXTE[s];
44
+ const zeilen = a.offen.length === 0
45
+ ? [t.noOpen]
46
+ : a.offen.map((o) => (o.art === "thema" ? t.openTopic(o.nummer, o.notiz) : t.openVerdict(o.nummer, o.spec ?? "", o.story ?? "", o.runde ?? 0, o.notiz)));
47
+ if (a.bestaetigt.length > 0)
48
+ zeilen.push(t.confirmed(a.bestaetigt.map((b) => t.confirmedItem(b.spec, b.story, b.runde)).join(", ")));
49
+ zeilen.push(t.view(a.adresse));
50
+ return zeilen;
51
+ }
52
+ /** Die Datei fehlt oder ist kein JSON -- Satz auf stderr, `null`. Sonst die gelesene Datei. */
53
+ export function planDateiOderAbsage(s) {
54
+ const datei = lesePlanDatei(process.cwd());
55
+ if (datei.ok)
56
+ return datei;
57
+ console.error(datei.grund === "fehlt" ? PLAN_TEXTE[s].fileMissing : PLAN_TEXTE[s].noJson);
58
+ return null;
59
+ }
60
+ /**
61
+ * Was beide Befehle mit einer Absage tun. `409`/`422` tragen einen fertigen Satz in der Kontosprache
62
+ * (Projekt unbekannt oder mehrdeutig, Plan unlesbar, Runde kleiner, veraltet) -- woertlich ausgeben, `1`.
63
+ * `503 teilweise` heisst: gespeichert, aber nicht ganz -- `3`, denn ein zweiter Lauf hilft (Spec Abschnitt 5).
64
+ */
65
+ export function planAbsage(antwort, s) {
66
+ const meldung = feld(antwort.koerper, "meldung") ?? ALLGEMEIN_TEXTE[s].genericFailure;
67
+ if (antwort.status === 402) {
68
+ console.error(meldung);
69
+ return 4;
70
+ }
71
+ if (antwort.status === 401) {
72
+ console.error(`${meldung}${ALLGEMEIN_TEXTE[s].pleaseSignIn}`);
73
+ return 2;
74
+ }
75
+ if (antwort.status === 409 || antwort.status === 422) {
76
+ console.error(meldung);
77
+ return 1;
78
+ }
79
+ // `503 teilweise` landet ueber `schlussFehler` auf derselben `3` wie jeder andere Aussetzer -- mit dem Satz des Servers.
80
+ const schluss = schlussFehler(antwort.status, meldung, s);
81
+ console.error(schluss.text);
82
+ return schluss.wert;
83
+ }
84
+ export async function plan(argv) {
85
+ let s = sprache();
86
+ if (argv.includes("--help") || argv.includes("-h")) {
87
+ console.log(PLAN_TEXTE[s].listHelp);
88
+ return 0;
89
+ }
90
+ const datei = planDateiOderAbsage(s);
91
+ if (datei === null)
92
+ return 1;
93
+ const suche = new URLSearchParams();
94
+ if (typeof datei.json.projekt_id === "string")
95
+ suche.set("projekt-id", datei.json.projekt_id);
96
+ if (typeof datei.json.projekt === "string")
97
+ suche.set("projekt", datei.json.projekt);
98
+ const antwort = await ruf(`/api/cli/projekt/plan?${suche.toString()}`, { methode: "GET", mitAusweis: true }); // kein-anzeigetext: Pfad und HTTP-Methode
99
+ // 🌐 Die Antwort kann die Kontosprache umgestellt haben: Was ab hier gedruckt wird, in der neuen.
100
+ s = sprache();
101
+ if (antwort.status !== 200)
102
+ return planAbsage(antwort, s);
103
+ const a = planAntwortAus(antwort.koerper);
104
+ if (a === null) {
105
+ console.error(ALLGEMEIN_TEXTE[s].unreadableAnswer);
106
+ return 3;
107
+ }
108
+ planZeilen(a, s).forEach((z) => console.log(z));
109
+ return 0;
110
+ }
@@ -10,6 +10,7 @@ import { sprache } from "../sprache.js";
10
10
  import { andereSpracheZuRaeumen, skillQuelle } from "../slash-befehle.js";
11
11
  import { SKILL_TEXTE } from "../texte/skill.js";
12
12
  import { frischeCoachAuf } from "../coach-ablegen.js";
13
+ import { frischeLogoAuf } from "../logo-ablegen.js";
13
14
  export const VERALTETE_BEFEHLE = [
14
15
  /*
15
16
  🔴 **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
@@ -325,11 +326,13 @@ export async function skillInstall() {
325
326
  * Der Befehl `kimal3 skill install`: die Anleitung ablegen und einen VORHANDENEN Coach mitziehen (Update, Sprachwechsel;
326
327
  * angelegt wird keiner). Bewusst hier und nicht in `skillInstall()`: `einrichten` ruft das auch, hat seinen Coach aber
327
328
  * schon selbst abgelegt und gemeldet -- ein zweites Auffrischen druckte dessen Zeile noch einmal (bei fehlender
328
- * Trenn-Marke auch die Ersatzdatei ein zweites Mal).
329
+ * Trenn-Marke auch die Ersatzdatei ein zweites Mal). Das Logo zieht genauso mit, nur ein vorhandenes (Spec 2026-10-18,
330
+ * L-3 bis L-7): `einrichten` legt es selbst ab.
329
331
  */
330
332
  export async function skillInstallBefehl() {
331
333
  const code = await skillInstall();
332
334
  frischeCoachAuf(sprache());
335
+ frischeLogoAuf(sprache());
333
336
  return code;
334
337
  }
335
338
  /**
@@ -0,0 +1,104 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, win32 } from "node:path";
3
+ import { zerlege } from "../argumente.js";
4
+ import { AUFNEHMER_FASSUNG, aufnehmerArgumente, einrichtPlan, teileAus, uvEinrichtung, } from "../zeigen-rechnung.js";
5
+ import { LESEN_MS, aufnehmerPython, findeUv, rufeWerkzeug, stillAusfuehren, uvBinOrdner } from "../zeigen-umgebung.js";
6
+ import { vorab } from "./zeigen.js";
7
+ /**
8
+ * `kimal3 zeigen einrichten` / `kimal3 show setup` (Spec 2026-10-18-k3zeigen-design.md, US-2, Szenarien S-1, S-3, S-15).
9
+ * Holt in dieser Reihenfolge nur, was fehlt: uv, den Aufnehmer 1.3.0, FFmpeg, das Sprachmodell. Ohne Adminrechte.
10
+ * 🔴 Kein fremdes Programm spricht hier mit dem Menschen: winget, uv und capture laufen mit `stdio: "ignore"`, vom
11
+ * Werkzeug zaehlt nur die eine JSON-Zeile. Ob ein Schritt geklappt hat, sagt nie sein Rueckgabewert allein, sondern
12
+ * die neue Bestandsaufnahme danach -- winget meldet z. B. auch "schon installiert" als Fehler.
13
+ */
14
+ // Obergrenzen je Schritt: grosszuegig (langsame Leitung), aber nie "haengt fuer immer". Der Slash-Befehl startet
15
+ // `einrichten` im Hintergrund, weil Claude Code einen Werkzeugaufruf nach 10 Minuten abbricht (Spec US-2).
16
+ const MINUTE = 60_000;
17
+ const ZEIT_UV = 15 * MINUTE;
18
+ const ZEIT_AUFNEHMER = 45 * MINUTE;
19
+ const ZEIT_FFMPEG = 15 * MINUTE;
20
+ const ZEIT_MODELL = 60 * MINUTE;
21
+ /** Was jetzt da ist: uv (Suchpfad + bekannte Orte), und vom Aufnehmer selbst, was er hat. */
22
+ function bestand() {
23
+ const p = aufnehmerPython();
24
+ return { teile: teileAus(findeUv()?.uv ?? null, p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS)), umgebungDa: p !== null };
25
+ }
26
+ function richteUvEin() {
27
+ const { befehl, argumente } = uvEinrichtung(stillAusfuehren("winget", ["--version"], LESEN_MS)); // kein-anzeigetext: Befehl
28
+ stillAusfuehren(befehl, argumente, ZEIT_UV);
29
+ // Der Suchpfad dieser Sitzung ist noch der alte: findeUv sieht auch an den bekannten Orten nach.
30
+ return findeUv() !== null;
31
+ }
32
+ function richteAufnehmerEin(umgebungDa) {
33
+ const uv = findeUv();
34
+ if (uv === null)
35
+ return false;
36
+ stillAusfuehren(uv.uv, aufnehmerArgumente(umgebungDa), ZEIT_AUFNEHMER);
37
+ return bestand().teile.aufnehmer === AUFNEHMER_FASSUNG;
38
+ }
39
+ function richteFfmpegEin() {
40
+ const uv = findeUv();
41
+ const p = aufnehmerPython();
42
+ if (uv === null || p === null)
43
+ return false;
44
+ const bin = uvBinOrdner(uv.uv);
45
+ // `capture.exe` liegt im Programmordner von uv und neben dem Python des Aufnehmers; der erste, den es gibt.
46
+ const capture = [bin === null ? null : win32.join(bin, "capture.exe"), win32.join(dirname(p.python), "capture.exe")] // kein-anzeigetext: Dateiname
47
+ .find((d) => d !== null && existsSync(d));
48
+ if (capture === undefined)
49
+ return false;
50
+ stillAusfuehren(capture, ["install-ffmpeg"], ZEIT_FFMPEG); // kein-anzeigetext: Befehl
51
+ return bestand().teile.ffmpeg;
52
+ }
53
+ function richteModellEin() {
54
+ const p = aufnehmerPython();
55
+ if (p === null)
56
+ return false;
57
+ const r = rufeWerkzeug(p, ["modell-laden"], null, ZEIT_MODELL); // kein-anzeigetext: Befehl
58
+ return r.ok && (r.daten.ergebnis === "da" || r.daten.ergebnis === "geladen");
59
+ }
60
+ function zeile(t, schritt) {
61
+ if (schritt === "uv")
62
+ return t.schrittUv;
63
+ if (schritt === "aufnehmer")
64
+ return t.schrittAufnehmer(AUFNEHMER_FASSUNG);
65
+ return schritt === "ffmpeg" ? t.schrittFfmpeg : t.schrittModell;
66
+ }
67
+ export async function zeigenEinrichten(rest) {
68
+ const lage = vorab(rest, (t) => t.helpEinrichten);
69
+ if (typeof lage === "number")
70
+ return lage;
71
+ const { s, t } = lage;
72
+ const z = zerlege(rest, [], s);
73
+ if (!z.ok) {
74
+ console.error(`${z.meldung}\n${t.helpEinrichten}`);
75
+ return 1;
76
+ }
77
+ const anfang = bestand();
78
+ const plan = einrichtPlan(anfang.teile);
79
+ if (plan.length === 0) {
80
+ console.log(t.allesDa(AUFNEHMER_FASSUNG));
81
+ return 0;
82
+ }
83
+ for (const schritt of plan) {
84
+ // FFmpeg und Modell erst nach einem neuen Blick: Ein frisch eingerichteter Aufnehmer findet z. B. ein FFmpeg im Suchpfad.
85
+ if ((schritt === "ffmpeg" || schritt === "modell") && bestand().teile[schritt])
86
+ continue;
87
+ console.log(zeile(t, schritt));
88
+ const geklappt = schritt === "uv" ? richteUvEin()
89
+ : schritt === "aufnehmer" ? richteAufnehmerEin(anfang.umgebungDa)
90
+ : schritt === "ffmpeg" ? richteFfmpegEin() : richteModellEin();
91
+ if (!geklappt) {
92
+ console.error(t.nichtGeklappt(schritt));
93
+ return 1;
94
+ }
95
+ }
96
+ // Die Schlussmeldung nur, wenn die Bestandsaufnahme danach wirklich alle vier zeigt -- nie aus dem Plan geschlossen.
97
+ const offen = einrichtPlan(bestand().teile);
98
+ if (offen.length > 0) {
99
+ console.error(t.nichtGeklappt(offen[0]));
100
+ return 1;
101
+ }
102
+ console.log(t.fertigEingerichtet(AUFNEHMER_FASSUNG));
103
+ return 0;
104
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { trenneNamenUndOptionen, zerlege } from "../argumente.js";
4
+ import { einrichtPlan, fehlerGrund } from "../zeigen-rechnung.js";
5
+ import { LESEN_MS, aufnehmerPython, leseStarter, rufeWerkzeug, schreibeNotiz, starteStarter, starterFakt, uvFassung } from "../zeigen-umgebung.js";
6
+ import { ermittleZustand, grundSatz, gueltigeFassung, ortHier, uhrzeit, vorab, zeigeOrdner } from "./zeigen.js";
7
+ /**
8
+ * `kimal3 zeigen start "<Satz>" [--hoechstens N]` (Spec US-3, US-4, US-5). Reihenfolge: Ort -> laeuft schon etwas? ->
9
+ * eingerichtet? -> Ordner -> Starter abgehaengt -> warten, bis er "laeuft, Sitzung …" meldet.
10
+ * 🔴 Der Satz geht nur nach `aufgabe.txt`, nie an den Aufnehmer (dessen Befehlszeile zerlegt ihn, Spec US-4).
11
+ * 🔴 Scheitert der Start, zieht `start` die Bremse (`abbrechen`) und wartet, bis der Starter das Ende bestaetigt -- ein
12
+ * Fehlschlag darf keinen Aufnehmer zuruecklassen. Gemeldet wird nur ein fester Grund und der Pfad zum Protokoll.
13
+ */
14
+ const HOECHSTENS_VORGABE = 15;
15
+ const TAKT_MS = 500;
16
+ const WARTEN_START_MS = 60_000; // der Aufnehmer selbst wartet bis 60 s auf seine Bereitschaft
17
+ const WARTEN_ENDE_MS = 25_000; // R-14, Punkt 4: Wegraeumen samt Nachtoeten braucht laenger als 15 s
18
+ const warte = (ms) => new Promise((fertig) => setTimeout(fertig, ms));
19
+ const laeuftNoch = (zustand) => zustand === "startet" || zustand === "laeuft";
20
+ /** Bis `fertig` den Bericht des Starters annimmt (oder die Zeit um ist); der letzte gelesene Bericht. */
21
+ async function warteAufStarter(ordner, ms, fertig) {
22
+ const bis = Date.now() + ms;
23
+ let fakt = starterFakt(leseStarter(ordner));
24
+ while (!fertig(fakt) && Date.now() < bis) {
25
+ await warte(TAKT_MS);
26
+ fakt = starterFakt(leseStarter(ordner));
27
+ }
28
+ return fakt;
29
+ }
30
+ function legeOrdnerAn(t, aufnahmen, ordner, anzeige) {
31
+ try {
32
+ mkdirSync(aufnahmen, { recursive: true });
33
+ // Git UND Claudes Dateisuche fernhalten (Spec US-3); nur anlegen, was fehlt.
34
+ for (const name of [".gitignore", ".ignore"]) {
35
+ const datei = join(aufnahmen, name);
36
+ if (!existsSync(datei))
37
+ writeFileSync(datei, "*\n", "utf8");
38
+ }
39
+ mkdirSync(ordner); // ohne `recursive`: Gibt es ihn schon, war ein zweiter Start schneller (Review Focus 4)
40
+ return null;
41
+ }
42
+ catch (e) {
43
+ return e.code === "EEXIST" ? t.startetGerade(anzeige) : t.ordnerNichtAnlegbar(ordner);
44
+ }
45
+ }
46
+ export async function zeigenStart(rest) {
47
+ const lage = vorab(rest, (t) => t.helpStart);
48
+ if (typeof lage === "number")
49
+ return lage;
50
+ const { s, t } = lage;
51
+ const { woerter, optionen } = trenneNamenUndOptionen(rest);
52
+ const satz = woerter.join(" ").trim();
53
+ const z = zerlege(optionen, ["hoechstens"], s);
54
+ if (!z.ok || satz === "") {
55
+ console.error(`${z.ok ? t.satzFehlt : z.meldung}\n${t.helpStart}`);
56
+ return 1;
57
+ }
58
+ const roh = z.werte.optionen.hoechstens ?? String(HOECHSTENS_VORGABE);
59
+ const minuten = /^\d+$/.test(roh) ? Number(roh) : NaN;
60
+ if (!(minuten >= 1 && minuten <= HOECHSTENS_VORGABE)) {
61
+ console.error(t.hoechstensFalsch(roh));
62
+ return 1;
63
+ }
64
+ const jetzt = new Date();
65
+ const ort = ortHier(jetzt);
66
+ if (ort.art !== "ort") {
67
+ console.error(ort.art === "werkbank" ? t.ortWerkbank : t.ortZuLang(ort.laenge, ort.hoechstens));
68
+ return 1;
69
+ }
70
+ const p = aufnehmerPython();
71
+ if (p !== null) {
72
+ const { zustand } = ermittleZustand(p, ort, jetzt.getTime() / 1000);
73
+ if (zustand.art === "unbekannt") {
74
+ console.error(t.unbekanntKeinStart);
75
+ return 1;
76
+ }
77
+ if (zustand.art === "laeuft") {
78
+ const bekannt = !zustand.fremd && zustand.gestartet !== null && zustand.ordner !== null;
79
+ console.error(bekannt ? t.schonLaeuft(uhrzeit(zustand.gestartet), zeigeOrdner(zustand.ordner, ort)) : t.schonLaeuftFremd);
80
+ return 1;
81
+ }
82
+ }
83
+ const teile = p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS);
84
+ const fehlt = !teile?.ok || einrichtPlan({
85
+ uv: uvFassung(),
86
+ aufnehmer: gueltigeFassung(teile.daten.aufnehmer),
87
+ ffmpeg: teile.daten.ffmpeg === true,
88
+ modell: teile.daten.modell === true,
89
+ }).length > 0;
90
+ if (p === null || fehlt) {
91
+ console.error(t.nichtEingerichtet);
92
+ return 1;
93
+ }
94
+ const ordner = ort.ordner;
95
+ const anzeige = zeigeOrdner(ordner, ort);
96
+ const absage = legeOrdnerAn(t, ort.aufnahmen, ordner, anzeige);
97
+ if (absage !== null) {
98
+ console.error(absage);
99
+ return 1;
100
+ }
101
+ const konfig = join(ordner, "konfig.json"); // kein-anzeigetext: Dateiname
102
+ try {
103
+ writeFileSync(join(ordner, "aufgabe.txt"), satz, "utf8"); // kein-anzeigetext: Dateiname
104
+ writeFileSync(konfig, JSON.stringify({ ordner, hoechstens_sek: minuten * 60 }), "utf8");
105
+ }
106
+ catch {
107
+ console.error(t.ordnerNichtAnlegbar(ordner));
108
+ return 1;
109
+ }
110
+ const protokoll = join(ordner, "aufnahme.log"); // kein-anzeigetext: Dateiname
111
+ let bericht = starteStarter(p, konfig, ordner)
112
+ ? await warteAufStarter(ordner, WARTEN_START_MS, (f) => (f?.zustand === "laeuft" && f.sitzung !== undefined) || (f !== null && !laeuftNoch(f.zustand)))
113
+ : null;
114
+ const lauf = leseStarter(ordner);
115
+ if (bericht?.zustand === "laeuft" && bericht.sitzung !== undefined && typeof lauf?.gestartet === "number" && typeof lauf.frist === "number") {
116
+ schreibeNotiz({ sitzung: bericht.sitzung, ordner, gestartet: lauf.gestartet, frist: lauf.frist });
117
+ console.log(t.laeuftJetzt(anzeige, uhrzeit(lauf.frist)));
118
+ return 0;
119
+ }
120
+ // Nicht gestartet: Bremse ziehen und auf die Bestaetigung warten, dass nichts weiterlaeuft.
121
+ try {
122
+ writeFileSync(join(ordner, "abbrechen"), "", "utf8"); // kein-anzeigetext: Dateiname
123
+ }
124
+ catch {
125
+ /* ohne Bremse meldet unten "nicht bestaetigt" */
126
+ }
127
+ bericht = await warteAufStarter(ordner, WARTEN_ENDE_MS, (f) => f !== null && !laeuftNoch(f.zustand));
128
+ const grund = bericht?.grund ?? fehlerGrund(liesProtokoll(protokoll));
129
+ console.error(t.nichtGestartet(grundSatz(t, grund), protokoll));
130
+ // Ohne Bericht (Starter nie angelaufen) ist das Ende genauso wenig bestaetigt wie bei "laeuft noch".
131
+ if (bericht === null || laeuftNoch(bericht.zustand) || bericht.grund === "nicht-beendet")
132
+ console.error(t.endeUnbestaetigt);
133
+ return 1;
134
+ }
135
+ /** Nur fuer `fehlerGrund` -- das Protokoll traegt Fenstertitel und wird nie gezeigt. */
136
+ function liesProtokoll(pfad) {
137
+ try {
138
+ return readFileSync(pfad, "utf8");
139
+ }
140
+ catch {
141
+ return "";
142
+ }
143
+ }