kimal3 0.4.0 → 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.
package/README.md CHANGED
@@ -44,6 +44,8 @@ kimal3 projekt neu … Ein Projekt ins Erfolgsprofil eintragen
44
44
  kimal3 projekt liste Deine eingetragenen Projekte anzeigen
45
45
  kimal3 kurs Zeigen, wo du im Mini-Kurs stehst
46
46
  kimal3 firma daten Die Daten deiner Firma holen, damit Claude sie auswerten kann
47
+ kimal3 zeigen start Eine Aufnahme starten, während du eine Aufgabe vormachst
48
+ kimal3 zeigen status Nachsehen, ob gerade aufgenommen wird und ob der Aufnehmer eingerichtet ist
47
49
  kimal3 wache Läuft von selbst beim Start von Claude: prüft Zugang, Nachricht, Version, Skill
48
50
  kimal3 wache aus Die tägliche Prüfung abschalten — sie sagt dann nichts mehr
49
51
  kimal3 wache an Sie wieder einschalten
@@ -182,6 +184,8 @@ kimal3 project new … Add a project to your success profile
182
184
  kimal3 project list Show the projects you've added
183
185
  kimal3 course Show where you are in the mini-course
184
186
  kimal3 company data Fetch your company's data so Claude can analyse it
187
+ kimal3 show start Start a recording while you show a task
188
+ kimal3 show status Check whether a recording is running and whether the recorder is set up
185
189
  kimal3 watch Runs by itself when Claude starts: checks your access, messages, version and skill
186
190
  kimal3 watch off Switch the daily check off — it then says nothing at all
187
191
  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
  /**
@@ -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
+ }
@@ -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
+ }