kimal3 0.4.0-durchstich.3 → 0.5.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.
Files changed (50) hide show
  1. package/README.md +6 -0
  2. package/claude-plugins/.claude-plugin/marketplace.json +8 -8
  3. package/claude-plugins/README.md +7 -7
  4. package/claude-plugins/kimal3-logo/.claude-plugin/plugin.json +6 -6
  5. package/claude-plugins/kimal3-logo/hooks/hooks.json +1 -1
  6. package/claude-plugins/kimal3-logo/hooks/register.tsx +80 -80
  7. package/coach/de/kimal3-coach.md +167 -0
  8. package/coach/en/kimal3-coach.md +167 -0
  9. package/dist/arbeitsplatz-dateien.js +1 -1
  10. package/dist/arbeitsplatz-finden.js +115 -0
  11. package/dist/arbeitsplatz-suche.js +176 -0
  12. package/dist/befehle/einrichten.js +250 -32
  13. package/dist/befehle/kurs.js +77 -0
  14. package/dist/befehle/login.js +12 -1
  15. package/dist/befehle/skill-install.js +15 -0
  16. package/dist/befehle/zeigen-einrichten.js +104 -0
  17. package/dist/befehle/zeigen-start.js +143 -0
  18. package/dist/befehle/zeigen.js +288 -0
  19. package/dist/befehle-katalog-en.js +39 -0
  20. package/dist/befehle-katalog.js +39 -0
  21. package/dist/befehlswoerter.js +15 -0
  22. package/dist/coach-ablegen.js +130 -0
  23. package/dist/coach-rechnung.js +40 -0
  24. package/dist/kimal3.js +14 -2
  25. package/dist/logo-ablegen.js +26 -6
  26. package/dist/slash-befehle.js +1 -0
  27. package/dist/texte/coach.js +27 -0
  28. package/dist/texte/einrichten-marken.js +2 -2
  29. package/dist/texte/einrichten.js +58 -5
  30. package/dist/texte/kurs.js +21 -0
  31. package/dist/texte/zeigen.js +189 -0
  32. package/dist/zeigen-rechnung.js +226 -0
  33. package/dist/zeigen-umgebung.js +190 -0
  34. package/package.json +3 -1
  35. package/skill/SKILL.md +10 -5
  36. package/skill/commands/k3.md +4 -3
  37. package/skill/commands/k3feierabend.md +2 -3
  38. package/skill/commands/k3kurs.md +27 -39
  39. package/skill/commands/k3start.md +4 -3
  40. package/skill/commands/k3zeigen.md +303 -0
  41. package/skill/en/SKILL.md +10 -5
  42. package/skill/en/commands/k3.md +4 -3
  43. package/skill/en/commands/k3course.md +27 -40
  44. package/skill/en/commands/k3show.md +302 -0
  45. package/skill/en/commands/k3start.md +4 -3
  46. package/skill/en/commands/k3wrapup.md +3 -4
  47. package/zeigen/gemeinsam.py +82 -0
  48. package/zeigen/starter.py +163 -0
  49. package/zeigen/werkzeug.py +250 -0
  50. package/zeigen/zeitleiste.py +393 -0
@@ -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
+ }
@@ -5,6 +5,8 @@ import { sprache } from "../sprache.js";
5
5
  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
+ import { frischeCoachAuf } from "../coach-ablegen.js";
9
+ import { frischeLogoAuf } from "../logo-ablegen.js";
8
10
  import { stelleSkillUm } from "./skill-tausch.js";
9
11
  const warte = (ms) => new Promise((f) => setTimeout(f, ms));
10
12
  // 🌐 Die Sprache wird bei JEDER Ausgabe neu gelesen: Die Abhol-Antwort nennt die Kontosprache
@@ -27,7 +29,16 @@ export async function login() {
27
29
  console.log(t().loginOpen(BASIS_URL));
28
30
  console.log(t().loginEnterCode(start.nutzerCode));
29
31
  console.log(t().loginWaiting);
30
- return warteAufBestaetigung(start.geraeteCode);
32
+ const code = await warteAufBestaetigung(start.geraeteCode);
33
+ // Der Coach zieht mit der Kontosprache mit -- nur beim BEFEHL `kimal3 login`, nicht in der geteilten Warteschleife:
34
+ // `einrichten` meldet anschließend seinen eigenen Coach, eine zweite Zeile wäre doppelt (Gesamt-Durchsicht, Fund 4;
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) {
38
+ frischeCoachAuf(sprache());
39
+ frischeLogoAuf(sprache());
40
+ }
41
+ return code;
31
42
  }
32
43
  /**
33
44
  * Die Anmeldung für `kimal3 einrichten` (Spec 2026-10-11, S-2): Code und Adresse auf eigenen
@@ -9,6 +9,8 @@ import { eigeneVersion } from "../version.js";
9
9
  import { sprache } from "../sprache.js";
10
10
  import { andereSpracheZuRaeumen, skillQuelle } from "../slash-befehle.js";
11
11
  import { SKILL_TEXTE } from "../texte/skill.js";
12
+ import { frischeCoachAuf } from "../coach-ablegen.js";
13
+ import { frischeLogoAuf } from "../logo-ablegen.js";
12
14
  export const VERALTETE_BEFEHLE = [
13
15
  /*
14
16
  🔴 **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
@@ -320,6 +322,19 @@ export async function skillInstall() {
320
322
  }
321
323
  return 0;
322
324
  }
325
+ /**
326
+ * Der Befehl `kimal3 skill install`: die Anleitung ablegen und einen VORHANDENEN Coach mitziehen (Update, Sprachwechsel;
327
+ * angelegt wird keiner). Bewusst hier und nicht in `skillInstall()`: `einrichten` ruft das auch, hat seinen Coach aber
328
+ * schon selbst abgelegt und gemeldet -- ein zweites Auffrischen druckte dessen Zeile noch einmal (bei fehlender
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.
331
+ */
332
+ export async function skillInstallBefehl() {
333
+ const code = await skillInstall();
334
+ frischeCoachAuf(sprache());
335
+ frischeLogoAuf(sprache());
336
+ return code;
337
+ }
323
338
  /**
324
339
  * Kopiert die Slash-Befehle nach `~/.claude/commands/`.
325
340
  *
@@ -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
+ }
@@ -0,0 +1,288 @@
1
+ import { existsSync } from "node:fs";
2
+ import { platform } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { schalterOhneWert, trenneNamenUndOptionen, zerlege } from "../argumente.js";
5
+ import { sprache } from "../sprache.js";
6
+ import { ZEIGEN_TEXTE } from "../texte/zeigen.js";
7
+ import { AUFNEHMER_FASSUNG, aufnahmeOrt, aufnahmeZustand, bildName, dauerText, gueltigeFassung, starterLebt, stoppAusgang, zeitAusText, zustandOhneAufnehmer, } from "../zeigen-rechnung.js";
8
+ import { LESEN_MS, STOPPEN_MS, aufnehmerPython, juengsterOrdner, leseNotiz, leseStarter, loescheNotiz, rufeWerkzeug, starterFakt, uvFassung, } from "../zeigen-umgebung.js";
9
+ /** `--help` (auch auf dem Mac), dann die Plattform. Eine Zahl = fertig, sonst die Lage fuer den Befehl. */
10
+ export function vorab(rest, hilfe) {
11
+ const s = sprache();
12
+ const t = ZEIGEN_TEXTE[s];
13
+ if (rest.includes("--help") || rest.includes("-h")) {
14
+ console.log(hilfe(t));
15
+ return 0;
16
+ }
17
+ if (platform() !== "win32") {
18
+ console.error(t.nurWindows);
19
+ return 1;
20
+ }
21
+ return { s, t };
22
+ }
23
+ export const sekundenJetzt = () => Date.now() / 1000;
24
+ export const ortHier = (jetzt) => aufnahmeOrt(process.cwd(), jetzt, platform(), existsSync);
25
+ /** Ein Aufnahmeordner zum Zeigen: im Projekt relativ mit `/` (`material/aufnahmen/…`), sonst der volle Pfad. */
26
+ export function zeigeOrdner(ordner, ort) {
27
+ if (ort.art !== "ort")
28
+ return ordner;
29
+ const rel = relative(dirname(dirname(ort.aufnahmen)), ordner);
30
+ return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? ordner : rel.split("\\").join("/");
31
+ }
32
+ /** Epochensekunden -> "09:12" in Ortszeit. */
33
+ export function uhrzeit(sek) {
34
+ const d = new Date(sek * 1000);
35
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
36
+ }
37
+ /** Ein Code vom Werkzeug -> fester Satz. Was nicht in `gruende` steht, wird nie gezeigt, sondern `unbekannt` (R-14, Punkt 9). */
38
+ export function grundSatz(t, code) {
39
+ return typeof code === "string" && Object.hasOwn(t.gruende, code) ? t.gruende[code] : t.gruende.unbekannt;
40
+ }
41
+ /** Wohnt seit Task 5 in `zeigen-rechnung.ts` (auch `einrichten` braucht sie); hier weitergereicht fuer `start`. */
42
+ export { gueltigeFassung };
43
+ const istZahl = (w) => typeof w === "number" && Number.isFinite(w);
44
+ const istZaehlung = (w) => istZahl(w) && Number.isInteger(w) && w >= 0;
45
+ /**
46
+ * Welcher Ordner gehoert zur Frage? Die Notiz zuerst, sonst die juengste Aufnahme im Projekt. Eine Notiz, deren Ordner
47
+ * es nicht mehr gibt, zaehlt nicht (`veraltet`): Ein Aufnehmer, der noch schreibt, haelt seinen Ordner fest.
48
+ */
49
+ function ordnerZurFrage(ort) {
50
+ const gelesen = leseNotiz();
51
+ const notiz = gelesen !== null && existsSync(gelesen.ordner) ? gelesen : null;
52
+ const kandidat = notiz?.ordner ?? (ort.art === "ort" ? juengsterOrdner(ort.aufnahmen) : null);
53
+ return { ordner: kandidat !== null && existsSync(kandidat) ? kandidat : null, notiz, veraltet: gelesen !== null && notiz === null };
54
+ }
55
+ /** Die aktiven Sitzungen aus einer `zustand`-Antwort; `null` = weiss nicht. */
56
+ function aktiveAus(r) {
57
+ const liste = r.ok ? r.daten.aktive_sitzungen : null;
58
+ return Array.isArray(liste) && liste.every((x) => typeof x === "string") ? liste : null;
59
+ }
60
+ /**
61
+ * Laeuft eine Aufnahme? Mit dem Python des Aufnehmers fragt `zustand` ihn selbst; ohne zaehlen nur Notiz und Starter
62
+ * (Ruling R-13, `zustandOhneAufnehmer`).
63
+ */
64
+ export function ermittleZustand(p, ort, jetzt) {
65
+ const { ordner, notiz } = ordnerZurFrage(ort);
66
+ const starterImNotizOrdner = notiz !== null && ordner === notiz.ordner;
67
+ if (p === null) {
68
+ const starter = ordner === null ? null : starterFakt(leseStarter(ordner));
69
+ return { zustand: zustandOhneAufnehmer({ notiz, starter, starterImNotizOrdner }, jetzt), ordner };
70
+ }
71
+ const r = rufeWerkzeug(p, ["zustand", ordner ?? "-"], ordner, LESEN_MS);
72
+ const starter = r.ok ? starterFakt(r.daten.starter) : null;
73
+ return { zustand: aufnahmeZustand({ aktiveSitzungen: aktiveAus(r), notiz, starter, starterImNotizOrdner }, jetzt), ordner };
74
+ }
75
+ // ── status ────────────────────────────────────────────────────────────────────────────────────────────────────────
76
+ export async function zeigenStatus(rest) {
77
+ const lage = vorab(rest, (t) => t.helpStatus);
78
+ if (typeof lage === "number")
79
+ return lage;
80
+ const { s, t } = lage;
81
+ const z = zerlege(rest, [], s);
82
+ if (!z.ok) {
83
+ console.error(`${z.meldung}\n${t.helpStatus}`);
84
+ return 1;
85
+ }
86
+ const jetzt = new Date();
87
+ const ort = ortHier(jetzt);
88
+ const p = aufnehmerPython();
89
+ const uv = uvFassung();
90
+ const zeilen = [t.eingerichtet, uv === null ? t.uvFehlt : t.uvDa(uv)];
91
+ const teile = p === null ? null : rufeWerkzeug(p, ["teile"], null, LESEN_MS);
92
+ if (p === null)
93
+ zeilen.push(t.aufnehmerFehlt, t.teilUngeprueft("ffmpeg"), t.teilUngeprueft("modell"));
94
+ else if (!teile?.ok)
95
+ zeilen.push(t.teilUnbekannt("aufnehmer"), t.teilUnbekannt("ffmpeg"), t.teilUnbekannt("modell"));
96
+ else {
97
+ const roh = teile.daten.aufnehmer;
98
+ const f = gueltigeFassung(roh);
99
+ zeilen.push(roh === null ? t.aufnehmerFehlt
100
+ : f === null ? t.teilUnbekannt("aufnehmer")
101
+ : f === AUFNEHMER_FASSUNG ? t.aufnehmerDa(f) : t.aufnehmerAlt(f, AUFNEHMER_FASSUNG));
102
+ zeilen.push(t.ffmpeg(teile.daten.ffmpeg === true), t.modell(teile.daten.modell === true));
103
+ }
104
+ const { zustand, ordner } = ermittleZustand(p, ort, jetzt.getTime() / 1000);
105
+ let code = 0;
106
+ zeilen.push(t.aufnahme);
107
+ if (zustand.art === "unbekannt") {
108
+ zeilen.push(` ${t.unbekannt}`);
109
+ code = 1;
110
+ }
111
+ else if (zustand.art === "laeuft") {
112
+ const { gestartet, frist } = zustand;
113
+ if (zustand.fremd || gestartet === null || frist === null || zustand.ordner === null)
114
+ zeilen.push(t.laeuftFremd);
115
+ else {
116
+ const minuten = Math.max(0, Math.floor(jetzt.getTime() / 60000 - gestartet / 60));
117
+ zeilen.push(t.laeuftSeit(uhrzeit(gestartet), minuten, zeigeOrdner(zustand.ordner, ort), uhrzeit(frist)));
118
+ }
119
+ }
120
+ else {
121
+ zeilen.push(t.keineLaeuft);
122
+ const roh = ordner === null ? null : leseStarter(ordner);
123
+ if (ordner !== null && zustand.letzte === "selbst-gestoppt") {
124
+ const min = istZahl(roh?.frist) && istZahl(roh?.gestartet) ? Math.round((roh.frist - roh.gestartet) / 60) : 15;
125
+ zeilen.push(t.letzteSelbst(zeigeOrdner(ordner, ort), min));
126
+ }
127
+ if (ordner !== null && zustand.letzte === "abgebrochen")
128
+ zeilen.push(t.letzteAbgebrochen(zeigeOrdner(ordner, ort)));
129
+ }
130
+ if (ort.art === "ort")
131
+ zeilen.push(t.ortHier(ort.aufnahmen));
132
+ else
133
+ zeilen.push(ort.art === "werkbank" ? t.ortWerkbank : t.ortZuLang(ort.laenge, ort.hoechstens));
134
+ console.log(zeilen.join("\n"));
135
+ return code;
136
+ }
137
+ // ── stopp ─────────────────────────────────────────────────────────────────────────────────────────────────────────
138
+ export async function zeigenStopp(rest) {
139
+ const lage = vorab(rest, (t) => t.helpStopp);
140
+ if (typeof lage === "number")
141
+ return lage;
142
+ const { s, t } = lage;
143
+ const z = zerlege(rest, [], s);
144
+ if (!z.ok) {
145
+ console.error(`${z.meldung}\n${t.helpStopp}`);
146
+ return 1;
147
+ }
148
+ const ort = ortHier(new Date());
149
+ const { ordner, notiz, veraltet } = ordnerZurFrage(ort);
150
+ if (veraltet)
151
+ loescheNotiz(); // ihr Ordner ist weg: die Notiz zeigt ins Leere
152
+ const p = aufnehmerPython();
153
+ // Eigene Aufnahme = eine Notiz, oder ein Starter, der noch "startet"/"laeuft" sagt. Eine alte, laengst gestoppte
154
+ // Aufnahme ohne Notiz ist keine laufende.
155
+ const starter = ordner === null ? null : starterFakt(leseStarter(ordner));
156
+ const eigene = ordner !== null && (notiz !== null || starter?.zustand === "startet" || starter?.zustand === "laeuft");
157
+ if (!eigene) {
158
+ // R-13: ohne Aufnehmer kann nichts laufen, was kimal3 gestartet hat. Mit ihm fragen, ob eine FREMDE laeuft (Review, Punkt 2).
159
+ const aktive = p === null ? [] : aktiveAus(rufeWerkzeug(p, ["zustand", "-"], null, LESEN_MS));
160
+ if (aktive === null)
161
+ console.error(t.unbekannt);
162
+ else if (aktive.length > 0)
163
+ console.error(t.fremdLaeuft);
164
+ else
165
+ console.log(t.keineAufnahmeLaeuft);
166
+ return aktive?.length === 0 ? 0 : 1;
167
+ }
168
+ if (p === null) {
169
+ // Eine eigene Aufnahme ist bekannt, der Aufnehmer aber nicht zu finden: stoppen kann ich nicht, "keine" waere gelogen.
170
+ console.error(t.stoppUnklar);
171
+ return 1;
172
+ }
173
+ const r = rufeWerkzeug(p, ["stoppen", ordner], ordner, STOPPEN_MS);
174
+ const d = r.ok ? r.daten : {};
175
+ const zahlen = istZahl(d.dauer_sek) && istZaehlung(d.klicks) && istZaehlung(d.tasten) && typeof d.ton === "boolean"
176
+ ? t.zahlen(dauerText(d.dauer_sek), d.klicks, d.tasten, d.ton) : null;
177
+ const ausgang = stoppAusgang(d.ergebnis, zahlen !== null);
178
+ // Werkzeug stumm, unlesbar oder noch beim Abschliessen: nichts ist bestaetigt -- die Notiz bleibt, damit status und
179
+ // der naechste stopp die Aufnahme weiter finden (Endpruefung, Punkt 2).
180
+ if (ausgang.notizWeg && notiz?.ordner === ordner)
181
+ loescheNotiz();
182
+ const roh = leseStarter(ordner);
183
+ const min = istZahl(roh?.frist) && istZahl(roh?.gestartet) ? Math.round((roh.frist - roh.gestartet) / 60) : 15;
184
+ const satz = {
185
+ keine: t.keineAufnahmeLaeuft,
186
+ gestoppt: t.gestoppt(zahlen ?? ""),
187
+ "selbst-gestoppt": t.selbstGestoppt(min, zahlen ?? ""),
188
+ "nicht-sauber": t.nichtSauber(grundSatz(t, d.grund)),
189
+ abgebrochen: t.abgebrochen(grundSatz(t, d.grund), zahlen),
190
+ "wird-abgeschlossen": t.wirdAbgeschlossen,
191
+ unklar: t.stoppUnklar,
192
+ }[ausgang.satz];
193
+ (ausgang.code === 0 ? console.log : console.error)(satz);
194
+ return ausgang.code;
195
+ }
196
+ // ── auswerten und bild: der Aufnahmeordner ───────────────────────────────────────────────────────────────────────
197
+ /** Der genannte oder der juengste Aufnahmeordner, der nicht mehr laeuft -- sonst ein Satz auf stderr und `null`. */
198
+ function fertigerOrdner(t, genannt, ort) {
199
+ const ordner = genannt !== undefined ? resolve(genannt) : ort.art === "ort" ? juengsterOrdner(ort.aufnahmen) : null;
200
+ if (ordner === null)
201
+ console.error(t.keineAufnahmeHier);
202
+ else if (!existsSync(ordner))
203
+ console.error(t.ordnerFehlt(ordner));
204
+ // Nur ein LEBENDER Starter blockiert (R-3): ein toter mit stehengebliebenem "laeuft" gilt als abgebrochen (Review, Punkt 3).
205
+ else if (starterLebt(starterFakt(leseStarter(ordner)), sekundenJetzt()))
206
+ console.error(t.laeuftNoch);
207
+ else
208
+ return ordner;
209
+ return null;
210
+ }
211
+ export async function zeigenAuswerten(rest) {
212
+ const lage = vorab(rest, (t) => t.helpAuswerten);
213
+ if (typeof lage === "number")
214
+ return lage;
215
+ const { s, t } = lage;
216
+ const ohne = schalterOhneWert(rest, "ohne-getipptes");
217
+ const { woerter, optionen } = trenneNamenUndOptionen(ohne.rest);
218
+ const z = zerlege([...woerter.slice(1), ...optionen], ["sprache"], s);
219
+ if (!z.ok) {
220
+ console.error(`${z.meldung}\n${t.helpAuswerten}`);
221
+ return 1;
222
+ }
223
+ const erkennung = z.werte.optionen.sprache ?? s;
224
+ if (erkennung !== "de" && erkennung !== "en") {
225
+ console.error(t.spracheFalsch(erkennung));
226
+ return 1;
227
+ }
228
+ const ort = ortHier(new Date());
229
+ const ordner = fertigerOrdner(t, woerter[0], ort);
230
+ if (ordner === null)
231
+ return 1;
232
+ const p = aufnehmerPython();
233
+ if (p === null) {
234
+ console.error(t.nichtEingerichtet);
235
+ return 1;
236
+ }
237
+ console.log(t.werteAus);
238
+ const r = rufeWerkzeug(p, ["zeitleiste", ordner, erkennung, ...(ohne.gesetzt ? ["--ohne-getipptes"] : [])], ordner, null);
239
+ const d = r.ok ? r.daten : {};
240
+ const datei = join(ordner, "zeitleiste.md"); // kein-anzeigetext: Dateiname
241
+ if (d.ergebnis === "fertig" && istZahl(d.dauer_sek) && [d.saetze, d.klicks, d.tippstellen, d.passwortfelder].every(istZaehlung) && existsSync(datei)) {
242
+ const n = (w) => w;
243
+ console.log(t.zeitleisteFertig(dauerText(d.dauer_sek), n(d.saetze), n(d.klicks), n(d.tippstellen), n(d.passwortfelder), zeigeOrdner(datei, ort)));
244
+ return 0;
245
+ }
246
+ if (d.ergebnis === "keine-sprache")
247
+ console.error(t.keineSprache);
248
+ else if (d.ergebnis === "kein-ton")
249
+ console.error(t.keinTon);
250
+ else
251
+ console.error(t.zeitleisteFehler(grundSatz(t, d.grund)));
252
+ return 1;
253
+ }
254
+ export async function zeigenBild(rest) {
255
+ const lage = vorab(rest, (t) => t.helpBild);
256
+ if (typeof lage === "number")
257
+ return lage;
258
+ const { s, t } = lage;
259
+ const { woerter, optionen } = trenneNamenUndOptionen(rest);
260
+ const z = zerlege([...woerter.slice(2), ...optionen], [], s);
261
+ if (!z.ok || woerter.length === 0) {
262
+ console.error(`${z.ok ? "" : `${z.meldung}\n`}${t.helpBild}`);
263
+ return 1;
264
+ }
265
+ const sekunden = zeitAusText(woerter[0]);
266
+ if (sekunden === null) {
267
+ console.error(t.zeitFalsch(woerter[0]));
268
+ return 1;
269
+ }
270
+ const ort = ortHier(new Date());
271
+ const ordner = fertigerOrdner(t, woerter[1], ort);
272
+ if (ordner === null)
273
+ return 1;
274
+ const p = aufnehmerPython();
275
+ if (p === null) {
276
+ console.error(t.nichtEingerichtet);
277
+ return 1;
278
+ }
279
+ const r = rufeWerkzeug(p, ["bild", ordner, String(sekunden)], ordner, null);
280
+ // Der Pfad wird hier gebaut, nicht vom Werkzeug uebernommen -- und muss danach wirklich daliegen.
281
+ const datei = join(ordner, "bilder", bildName(sekunden)); // kein-anzeigetext: Ordnername
282
+ if (r.ok && r.daten.ergebnis === "fertig" && existsSync(datei)) {
283
+ console.log(t.bildFertig(datei));
284
+ return 0;
285
+ }
286
+ console.error(t.bildFehler(grundSatz(t, r.ok ? r.daten.grund : null)));
287
+ return 1;
288
+ }