dante-watch 0.1.1 → 0.1.2
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/package.json +1 -1
- package/src/cli.mjs +4 -1
- package/src/core/sh.mjs +19 -1
- package/src/i18n.mjs +3 -3
- package/src/mascot.mjs +67 -43
- package/src/pack/install.mjs +31 -19
- package/src/session.mjs +61 -28
- package/src/ticker.mjs +18 -41
- package/src/ui/footer.mjs +46 -0
- package/src/ui/job-worker.mjs +18 -0
- package/src/ui/job.mjs +16 -0
- package/src/ui/quips.mjs +16 -0
- package/src/ui/wait.mjs +80 -0
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { ask, recordYes } from "./core/consent.mjs";
|
|
|
9
9
|
import { banner } from "./mascot.mjs";
|
|
10
10
|
import { activeBattery } from "./checks/battery.mjs";
|
|
11
11
|
import { demo } from "./checks/demo.mjs";
|
|
12
|
-
import { staleLine } from "./ticker.mjs";
|
|
12
|
+
import { staleLine, VERSION, BIB_DATE } from "./ticker.mjs";
|
|
13
13
|
import { lang, t } from "./i18n.mjs";
|
|
14
14
|
import { tx } from "./i18n-checks.mjs";
|
|
15
15
|
|
|
@@ -24,6 +24,7 @@ function parseArgs(argv) {
|
|
|
24
24
|
else if (t === "--fix") a.fix = true;
|
|
25
25
|
else if (t === "--demo") a.demo = true;
|
|
26
26
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
27
|
+
else if (t === "--version" || t === "-v") a.version = true;
|
|
27
28
|
}
|
|
28
29
|
return a;
|
|
29
30
|
}
|
|
@@ -34,6 +35,7 @@ const HELP = `Dante — wächter
|
|
|
34
35
|
npx dante-watch --ssh user@host [--key /pfad] prüft einen Server, der dir gehört
|
|
35
36
|
--fix bei einem Fund: Plan zeigen und nach deinem Ja schließen
|
|
36
37
|
--demo harmlose Übung: zeigt die ganze Schleife (human → plan → ja/nein → fix → Beweis)
|
|
38
|
+
--version welche Version läuft
|
|
37
39
|
--json Maschinen-Ausgabe
|
|
38
40
|
-y, --yes nicht nachfragen (für Automode/Cron)
|
|
39
41
|
|
|
@@ -78,6 +80,7 @@ async function handleFinding(check, r, a) {
|
|
|
78
80
|
async function main() {
|
|
79
81
|
const a = parseArgs(process.argv.slice(2));
|
|
80
82
|
if (a.help) { console.log(HELP); return; }
|
|
83
|
+
if (a.version) { console.log("dante-watch " + VERSION + " · bib " + BIB_DATE); return; }
|
|
81
84
|
|
|
82
85
|
// No flags + a real terminal → the face: summon, greet, arm, talk, stay open.
|
|
83
86
|
// Flags or a pipe → one-shot (for cron / --json / automode).
|
package/src/core/sh.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// sh: the only place a real line enters Dante. Runs a command, captures stdout/stderr/exit.
|
|
2
2
|
// Optionally prefixes with ssh so Dante can look at a remote server the user owns.
|
|
3
3
|
// Nothing here is a model's opinion — it is the machine's own output.
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
let PREFIX = null; // e.g. ["ssh","-i","/key","root@host"] — set by configureTarget()
|
|
7
7
|
|
|
@@ -36,6 +36,24 @@ export function runLocal(script, { timeout = 15000 } = {}) {
|
|
|
36
36
|
return exec(script, null, { timeout });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** runLocal() without blocking: the waiting screen keeps moving while the command runs. */
|
|
40
|
+
export function runLocalAsync(script, { timeout = 15000 } = {}) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const c = spawn("bash", ["-lc", String(script)], { stdio: ["ignore", "pipe", "pipe"] });
|
|
43
|
+
let out = "", err = "", done = false, killed = false;
|
|
44
|
+
const finish = (code, errorMsg = "") => {
|
|
45
|
+
if (done) return;
|
|
46
|
+
done = true; clearTimeout(timer);
|
|
47
|
+
const status = killed ? null : code;
|
|
48
|
+
resolve({ cmd: String(script), out: out.trimEnd(), err: err.trimEnd(), code: typeof status === "number" ? status : -1, ok: status === 0, ran: typeof status === "number" && status >= 0, failed: !!errorMsg, errorMsg });
|
|
49
|
+
};
|
|
50
|
+
const timer = setTimeout(() => { killed = true; c.kill("SIGKILL"); }, timeout);
|
|
51
|
+
c.stdout.on("data", (d) => (out += d)); c.stderr.on("data", (d) => (err += d));
|
|
52
|
+
c.on("error", (e) => finish(-1, String(e.message)));
|
|
53
|
+
c.on("close", (code) => finish(code));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
function exec(script, prefix, { timeout, input }) {
|
|
40
58
|
const s = Array.isArray(script) ? script.join(" ") : String(script);
|
|
41
59
|
const argv = prefix ? [...prefix, s] : ["bash", "-lc", s];
|
package/src/i18n.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const T = {
|
|
|
8
8
|
sLaw: "Jedes Urteil steht auf einer echten Zeile. Fehlt die Zeile, sagt Dante „unbekannt“, nie „sicher“.",
|
|
9
9
|
intro: "Ich bin Dante.", introSub: " Ich gehe durch die Hölle und bringe die Karte.",
|
|
10
10
|
arm: "Soll ich wachen?", armOpts: "[ja / nein]", sleepNo: "Dann schlafe ich. Beschwör mich, wenn du mich brauchst.",
|
|
11
|
-
armed: "Scharf.", watching: "Ich wache über ", sayHelp: ["Sag ", "hilfe", ", wenn du die Worte brauchst."],
|
|
11
|
+
armed: "Scharf.", awake: "wach", watching: "Ich wache über ", wakeStep: (i, n) => "weckt die Ohren " + i + "/" + n, wakeMore: "weckt die Fach-Ohren …", waitCheck: (x) => "prüfe " + x, waitLook: (x) => "schaue mir " + x + " an", sayHelp: ["Sag ", "hilfe", ", wenn du die Worte brauchst."],
|
|
12
12
|
unseen: (t, l) => `Ich sehe ${t} nicht: ${l}`, canFix: "Ich kann das nach deinem Ja schließen — sag ",
|
|
13
13
|
listen: " HUMAN, HÖR ZU ", what: "was:", how: "wie:", plan: "mein plan:",
|
|
14
14
|
distrust: "prüf das gegen eine andere KI. Sagt die was anderes — schreib uns: info@nm-streetwear.space",
|
|
@@ -34,7 +34,7 @@ const T = {
|
|
|
34
34
|
sLaw: "Every verdict stands on a real line. Without the line, Dante says „unknown“, never „safe“.",
|
|
35
35
|
intro: "I am Dante.", introSub: " I walk through Hell and bring back the map.",
|
|
36
36
|
arm: "Shall I keep watch?", armOpts: "[yes / no]", sleepNo: "Then I sleep. Summon me when you need me.",
|
|
37
|
-
armed: "Armed.", watching: "Watching over ", sayHelp: ["Say ", "help", " when you need the words."],
|
|
37
|
+
armed: "Armed.", awake: "awake", watching: "Watching over ", wakeStep: (i, n) => "waking the ears " + i + "/" + n, wakeMore: "waking the specialist ears …", waitCheck: (x) => "checking " + x, waitLook: (x) => "looking at " + x, sayHelp: ["Say ", "help", " when you need the words."],
|
|
38
38
|
unseen: (t, l) => `I cannot see ${t}: ${l}`, canFix: "I can close this after your yes — say ",
|
|
39
39
|
listen: " HUMAN, LISTEN ", what: "what:", how: "how:", plan: "my plan:",
|
|
40
40
|
distrust: "check this against another AI. If it says otherwise — write us: info@nm-streetwear.space",
|
|
@@ -60,7 +60,7 @@ const T = {
|
|
|
60
60
|
sLaw: "Cada veredicto se apoya en una línea real. Sin la línea, Dante dice „desconocido“, nunca „seguro“.",
|
|
61
61
|
intro: "Soy Dante.", introSub: " Atravieso el infierno y traigo el mapa.",
|
|
62
62
|
arm: "¿Vigilo?", armOpts: "[sí / no]", sleepNo: "Entonces duermo. Llámame cuando me necesites.",
|
|
63
|
-
armed: "Listo.", watching: "Vigilo ", sayHelp: ["Di ", "ayuda", " cuando necesites las palabras."],
|
|
63
|
+
armed: "Listo.", awake: "despierto", watching: "Vigilo ", wakeStep: (i, n) => "despertando los oídos " + i + "/" + n, wakeMore: "despertando los oídos especiales …", waitCheck: (x) => "revisando " + x, waitLook: (x) => "mirando " + x, sayHelp: ["Di ", "ayuda", " cuando necesites las palabras."],
|
|
64
64
|
unseen: (t, l) => `No veo ${t}: ${l}`, canFix: "Puedo cerrarlo después de tu sí — di ",
|
|
65
65
|
listen: " HUMANO, ESCUCHA ", what: "qué:", how: "cómo:", plan: "mi plan:",
|
|
66
66
|
distrust: "compruébalo con otra IA. Si dice otra cosa — escríbenos: info@nm-streetwear.space",
|
package/src/mascot.mjs
CHANGED
|
@@ -1,56 +1,80 @@
|
|
|
1
|
-
// Dante — the pilgrim who walked through Hell and came back with the map.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Dante — the pilgrim who walked through Hell and came back with the map. Virgil's companion: the same
|
|
2
|
+
// hooded guide, but he carries a sword. Drawn as a pixel sprite with 256-colour half-blocks (two pixel
|
|
3
|
+
// rows per text line), same renderer as Virgil. No library.
|
|
4
|
+
|
|
5
|
+
// 256-colour palette per legend char. "." = transparent.
|
|
4
6
|
const PAL = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
m: 230, // the map / parchment he brings back
|
|
13
|
-
M: 180, // map edge
|
|
7
|
+
H: 23, // hood / robe (deep teal)
|
|
8
|
+
B: 24, // robe body (calm blue)
|
|
9
|
+
F: 234, // face in shadow
|
|
10
|
+
S: 252, // sword blade (bright steel)
|
|
11
|
+
s: 245, // blade shadow
|
|
12
|
+
G: 214, // cross-guard (gold)
|
|
13
|
+
h: 94, // hilt (brown)
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// 18 rows × 17 cols → 9 text rows. Figure left, sword held upright on the right.
|
|
16
17
|
const ART = [
|
|
17
|
-
"
|
|
18
|
-
"..
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"....
|
|
22
|
-
"....
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
".
|
|
18
|
+
"....HHHH......S..",
|
|
19
|
+
"...HHHHHH.....S..",
|
|
20
|
+
"..HHHHHHHH....Ss.",
|
|
21
|
+
"..HHFFFFHH....Ss.",
|
|
22
|
+
"..HHFFFFHH....Ss.",
|
|
23
|
+
"..HHHFFHHH....Ss.",
|
|
24
|
+
".HHHHHHHHHH..GGGG",
|
|
25
|
+
".HHBBBBBBHH...hh.",
|
|
26
|
+
".HBBBBBBBBH...hh.",
|
|
27
|
+
".HBBBBBBBBH......",
|
|
28
|
+
".HBBBBBBBBH......",
|
|
29
|
+
".HBBBBBBBBH......",
|
|
30
|
+
".HHBBBBBBHH......",
|
|
31
|
+
".HHHBBBBHHH......",
|
|
32
|
+
".HHHHHHHHHH......",
|
|
33
|
+
"..HHHHHHHH.......",
|
|
34
|
+
"..HHHHHHHH.......",
|
|
35
|
+
"..HHHHHHHH.......",
|
|
31
36
|
];
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
function cell(top, bot) {
|
|
39
|
+
const t = PAL[top], b = PAL[bot];
|
|
40
|
+
if (t === undefined && b === undefined) return " ";
|
|
41
|
+
if (t !== undefined && b === undefined) return `\x1b[38;5;${t}m▀\x1b[0m`;
|
|
42
|
+
if (t === undefined && b !== undefined) return `\x1b[38;5;${b}m▄\x1b[0m`;
|
|
43
|
+
return `\x1b[38;5;${t}m\x1b[48;5;${b}m▀\x1b[0m`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The sprite as an array of ready-to-print lines. */
|
|
47
|
+
export function danteLines() {
|
|
48
|
+
const lines = [];
|
|
49
|
+
for (let r = 0; r < ART.length; r += 2) {
|
|
50
|
+
const top = ART[r], bot = ART[r + 1] ?? "";
|
|
37
51
|
let line = "";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const t = px(top[x]), b = px(bot[x]);
|
|
41
|
-
if (t == null && b == null) line += " ";
|
|
42
|
-
else if (t != null && b != null) line += `\x1b[38;5;${t};48;5;${b}m▀\x1b[0m`;
|
|
43
|
-
else if (t != null) line += `\x1b[38;5;${t}m▀\x1b[0m`;
|
|
44
|
-
else line += `\x1b[38;5;${b}m▄\x1b[0m`;
|
|
45
|
-
}
|
|
46
|
-
out.push(line);
|
|
52
|
+
for (let c = 0; c < top.length; c++) line += cell(top[c] ?? ".", bot[c] ?? ".");
|
|
53
|
+
lines.push(line);
|
|
47
54
|
}
|
|
48
|
-
return
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
export const danteSprite = () => danteLines().join("\n");
|
|
58
|
+
|
|
59
|
+
// The lettering, in the same small font as Virgil's: one glyph per letter, four rows each.
|
|
60
|
+
const GLYPH = {
|
|
61
|
+
D: [" ___ ", "| \\ ", "| |) |", "|___/ "],
|
|
62
|
+
A: [" _ ", " /_\\ ", " / _ \\ ", "/_/ \\_\\"],
|
|
63
|
+
N: [" _ _ ", "| \\| |", "| .` |", "|_|\\_|"],
|
|
64
|
+
T: [" _____ ", "|_ _|", " | | ", " |_| "],
|
|
65
|
+
E: [" ___ ", "| __|", "| _| ", "|___|"],
|
|
66
|
+
};
|
|
67
|
+
export const WORDMARK = [0, 1, 2, 3].map((row) => " " + [..."DANTE"].map((ch) => GLYPH[ch][row]).join(""));
|
|
68
|
+
|
|
69
|
+
// The start screen, line by line: the sprite, the lettering in teal, the sentence under it.
|
|
70
|
+
export function startLines(tagline) {
|
|
71
|
+
const teal = (s) => `\x1b[38;5;30m\x1b[1m${s}\x1b[0m`;
|
|
72
|
+
return ["", ...danteLines().map((l) => " " + l), "", ...WORDMARK.map(teal), "", " " + tagline, ""];
|
|
49
73
|
}
|
|
50
74
|
|
|
75
|
+
// One-shot runs (cron, a pipe): the plain name; in a terminal the sprite above it.
|
|
51
76
|
export function banner() {
|
|
52
77
|
const tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
53
78
|
const name = tty ? "\x1b[1mDante\x1b[0m \x1b[2m· er geht durch die Hölle und bringt die Karte\x1b[0m" : "Dante · wächter";
|
|
54
|
-
|
|
55
|
-
return danteSprite() + "\n" + name;
|
|
79
|
+
return tty ? danteSprite() + "\n" + name : name;
|
|
56
80
|
}
|
package/src/pack/install.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { readFileSync, writeFileSync, mkdirSync, renameSync, mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { runLocal } from "../core/sh.mjs";
|
|
9
|
+
import { runLocal, runLocalAsync } from "../core/sh.mjs";
|
|
10
10
|
import { home, packsDir, packFile, verifyPack, loadPacks } from "./pack.mjs";
|
|
11
11
|
import { rulesWith } from "../checks/battery.mjs";
|
|
12
12
|
|
|
@@ -74,23 +74,35 @@ export const curlShown = (url = PACKS_URL) => `curl -sS --max-time 30 -o <tmp> -
|
|
|
74
74
|
|
|
75
75
|
// The one fetch: ONE call returns every pack this person owns as {"packs":[…]}. Returns
|
|
76
76
|
// { ok, code, raws } — code is the HTTP status (0 = no answer), raws the pack texts on 200.
|
|
77
|
-
|
|
77
|
+
// What to run (the header goes to curl on stdin with -K -, so the token is not in curl's arguments):
|
|
78
|
+
function curlPlan(url) {
|
|
78
79
|
const token = readLicense();
|
|
79
|
-
if (!token) return
|
|
80
|
-
const dir = mkdtempSync(join(tmpdir(), "dante-"));
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
if (!token) return null;
|
|
81
|
+
const dir = mkdtempSync(join(tmpdir(), "dante-")), out = join(dir, "packs.json");
|
|
82
|
+
return { dir, out, script: `printf 'header = "Authorization: Bearer %s"\\n' "$(cat ${sq(licenseFile())})" | curl -sS -K - --max-time 30 --max-filesize ${MAX_DOWNLOAD} -o ${sq(out)} -w '%{http_code}' ${sq(url)}` };
|
|
83
|
+
}
|
|
84
|
+
// What came back (r = the finished command, out = the file curl wrote):
|
|
85
|
+
function curlResult(r, out) {
|
|
86
|
+
if (r.code === 127) return { ok: false, code: 0, curlMissing: true };
|
|
87
|
+
const code = Number((r.out || "").trim().slice(-3)) || 0;
|
|
88
|
+
if (code !== 200) return { ok: false, code, err: r.err };
|
|
89
|
+
let list;
|
|
90
|
+
try { list = JSON.parse(readFileSync(out, "utf8")).packs; } catch { return { ok: false, code: 200, badBody: true }; }
|
|
91
|
+
if (!Array.isArray(list)) return { ok: false, code: 200, badBody: true };
|
|
92
|
+
return { ok: true, code, raws: list.map((p) => JSON.stringify(p)) };
|
|
93
|
+
}
|
|
94
|
+
export function fetchPacks({ url = PACKS_URL } = {}) {
|
|
95
|
+
const plan = curlPlan(url);
|
|
96
|
+
if (!plan) return { ok: false, code: -1, noLicense: true };
|
|
97
|
+
try { return curlResult(runLocal(plan.script, { timeout: 40000 }), plan.out); }
|
|
98
|
+
catch (e) { return { ok: false, code: 0, err: String(e.message || e) }; }
|
|
99
|
+
finally { rmSync(plan.dir, { recursive: true, force: true }); }
|
|
100
|
+
}
|
|
101
|
+
// The same without blocking, so the waiting screen can keep running while curl waits for the server.
|
|
102
|
+
export async function fetchPacksAsync({ url = PACKS_URL } = {}) {
|
|
103
|
+
const plan = curlPlan(url);
|
|
104
|
+
if (!plan) return { ok: false, code: -1, noLicense: true };
|
|
105
|
+
try { return curlResult(await runLocalAsync(plan.script, { timeout: 40000 }), plan.out); }
|
|
106
|
+
catch (e) { return { ok: false, code: 0, err: String(e.message || e) }; }
|
|
107
|
+
finally { rmSync(plan.dir, { recursive: true, force: true }); }
|
|
96
108
|
}
|
package/src/session.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// The face: you summon Dante, he
|
|
1
|
+
// The face: you summon Dante, he is awake — sprite, lettering, a loading screen that wakes his ears, then the chat
|
|
2
|
+
// with the loading-screen tips pinned under it. He stays open until you say "raus".
|
|
2
3
|
// Local and honest — no LLM, no network of its own. The router hears what you mean (with a
|
|
3
4
|
// probability); when it is not sure it asks back instead of acting. He waits until you say "raus".
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
@@ -9,21 +10,24 @@ import { configureTarget, targetLabel, probe } from "./core/sh.mjs";
|
|
|
9
10
|
import { renderResult } from "./core/render.mjs";
|
|
10
11
|
import { RISK, WARN, SAFE } from "./core/check.mjs";
|
|
11
12
|
import { ask, recordYes, grantAutomode } from "./core/consent.mjs";
|
|
12
|
-
import {
|
|
13
|
+
import { startLines } from "./mascot.mjs";
|
|
13
14
|
import { activeBattery, checkById, runCheck } from "./checks/battery.mjs";
|
|
14
15
|
import { demo } from "./checks/demo.mjs";
|
|
15
16
|
import { run, firstLine } from "./core/sh.mjs";
|
|
16
17
|
import { slots } from "./bib/router.mjs";
|
|
17
|
-
import { hearMain, hearConsent, hearSystem, hearWissen, hearDomain, langOf } from "./bib/ear.mjs";
|
|
18
|
+
import { hearMain, hearConsent, hearSystem, hearMuster, hearWissen, hearDomain, langOf } from "./bib/ear.mjs";
|
|
18
19
|
import { cardsOf, findCard, answer as knowAnswer } from "./pack/know.mjs";
|
|
19
20
|
import { FAMILY_OF, RECIPES } from "./bib/system.mjs";
|
|
20
21
|
import { ANSWERS } from "./bib/answers.mjs";
|
|
21
|
-
import {
|
|
22
|
+
import { setKnowledge } from "./ticker.mjs";
|
|
23
|
+
import { waitFor } from "./ui/wait.mjs";
|
|
24
|
+
import { footerStart, footerStop, footerNext } from "./ui/footer.mjs";
|
|
25
|
+
import { job } from "./ui/job.mjs";
|
|
22
26
|
import { t, lang, setLang, COMMANDS } from "./i18n.mjs";
|
|
23
27
|
import { tx } from "./i18n-checks.mjs";
|
|
24
28
|
import { hear } from "./core/consent.mjs";
|
|
25
29
|
import { pt, ruleTitles } from "./pack/texts.mjs";
|
|
26
|
-
import { readLicense, saveLicense,
|
|
30
|
+
import { readLicense, saveLicense, fetchPacksAsync, installMany, curlShown, TOKEN_IN_LINE } from "./pack/install.mjs";
|
|
27
31
|
import { packsDir } from "./pack/pack.mjs";
|
|
28
32
|
import { readdirSync } from "node:fs";
|
|
29
33
|
|
|
@@ -38,33 +42,54 @@ const pick = (xs) => xs[Math.floor(Math.random() * xs.length)];
|
|
|
38
42
|
|
|
39
43
|
export async function session() {
|
|
40
44
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
45
|
+
// However the session ends (raus, Ctrl-D, Ctrl-C), the terminal must come back: the footer's scroll region is
|
|
46
|
+
// reset, or the screen would stay frozen.
|
|
47
|
+
rl.on("close", footerStop);
|
|
48
|
+
rl.on("SIGINT", () => { footerStop(); process.stdout.write("\n"); rl.close(); process.exit(0); });
|
|
41
49
|
let target = null; // null = localhost
|
|
42
50
|
let key = null;
|
|
43
51
|
let last = []; // last results, for "fix" and "karte"
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
// ---- the start: there is no door to knock on, he is awake ----
|
|
54
|
+
const dim = (x) => C("2", x);
|
|
55
|
+
const bar = (i, n) => { const w = 16, f = Math.round((w * i) / n); return C("36", "▕" + "▓".repeat(f) + "░".repeat(w - f) + "▏"); };
|
|
56
|
+
const frame = (text) => { if (process.stdout.isTTY && !process.env.DANTE_NO_WAIT) process.stdout.write("\r\x1b[2K" + text); };
|
|
57
|
+
say(startLines(C("1", t("intro")) + dim(t("introSub"))).join("\n"));
|
|
58
|
+
// The loading screen: a bar that fills as each ear really wakes (they learn their books now, not in the middle
|
|
59
|
+
// of the first answer). Only the two every start needs; the specialist ears wake the first time they are asked.
|
|
60
|
+
const EARS = [() => hearMain(""), () => hearConsent("")];
|
|
61
|
+
for (let i = 0; i <= EARS.length; i++) {
|
|
62
|
+
frame(" " + bar(i, EARS.length) + " 🦖 " + dim(t("wakeStep")(i, EARS.length)));
|
|
63
|
+
if (i < EARS.length) EARS[i]();
|
|
64
|
+
}
|
|
65
|
+
frame("");
|
|
66
|
+
let domainWarm = false;
|
|
67
|
+
const warmDomain = () => {
|
|
68
|
+
if (domainWarm) return;
|
|
69
|
+
domainWarm = true;
|
|
70
|
+
for (const [i, ear] of [hearMuster, hearSystem, hearWissen].entries()) { frame(" " + bar(i, 3) + " 🦖 " + dim(t("wakeMore"))); ear(""); }
|
|
71
|
+
frame("");
|
|
72
|
+
};
|
|
73
|
+
// The rules Dante runs with: the built-in ones, plus every pack in ~/.dante/packs that passes every check.
|
|
53
74
|
let { battery, packs: activePacks, notices } = activeBattery();
|
|
54
75
|
const clock = () => { if (activePacks.length) setKnowledge(activePacks[activePacks.length - 1].issued); }; // the clock then counts from the newest pack
|
|
55
76
|
clock();
|
|
56
77
|
|
|
57
78
|
configureTarget(null);
|
|
58
79
|
const [h0, h1, h2] = t("sayHelp");
|
|
59
|
-
say(" " + C("32", t("
|
|
80
|
+
say(" " + C("32", "● " + t("awake")) + dim(" · ") + dim(t("watching")) + targetLabel() + ". " + dim(h0) + C("1", h1) + dim(h2));
|
|
60
81
|
for (const n of notices) say(" " + C("33", pt().ignored(n.file, pt().reason(n.code, n.detail))));
|
|
61
|
-
if (activePacks.length) say(" " +
|
|
82
|
+
if (activePacks.length) say(" " + dim(pt().on(activePacks)));
|
|
83
|
+
footerStart(); // the loading-screen tips, pinned under the chat
|
|
62
84
|
|
|
85
|
+
// The slow work (ssh, waiting for the server) runs in a worker thread, so the waiting screen keeps moving.
|
|
86
|
+
const target$ = () => (target ? { host: target, key } : null);
|
|
87
|
+
const brokeOff = (c) => ({ id: c.id, title: c.title, verdict: "unknown", was: "Check brach ab.", wie: "", wo: "", evidence: [] });
|
|
63
88
|
// no list given = the whole battery: Dante's job is every thing he watches, not only one hole
|
|
64
89
|
const runChecks = async (withFix, checks = battery) => {
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
67
|
-
last = checks.map((c) => ({ c, r:
|
|
90
|
+
const res = await waitFor(t("waitCheck")(targetLabel()), job({ kind: "checks", ids: checks.map((c) => c.id) }, target$()));
|
|
91
|
+
if (!res.reached) { say(" " + C("100;37", t("rUnknown")) + " " + t("unseen")(targetLabel(), res.line)); return; }
|
|
92
|
+
last = checks.map((c, i) => ({ c, r: res.results[i] || brokeOff(c) }));
|
|
68
93
|
for (const { r } of last) say(renderResult(r));
|
|
69
94
|
const open = last.filter(({ r }) => r.verdict === RISK || r.verdict === WARN && r.fix);
|
|
70
95
|
if (open.length && !withFix) say("\n " + C("2", t("canFix")) + C("1", "fix") + C("2", "."));
|
|
@@ -81,12 +106,16 @@ export async function session() {
|
|
|
81
106
|
if (a === "no") { say(" " + C("2", t("nothing"))); return; }
|
|
82
107
|
if (a === "always") { grantAutomode(check.id); }
|
|
83
108
|
for (const s of r.fix.steps) {
|
|
84
|
-
const
|
|
109
|
+
const o = await waitFor(tx(s.desc), job({ kind: "exec", scripts: [s.script], timeout: 120000 }, target$()));
|
|
110
|
+
const res = o.reached ? o.outs[0] : { ok: false, out: "", err: o.line };
|
|
85
111
|
say(" " + (res.ok ? C("32", "✓") : C("31", "✗")) + " " + tx(s.desc) + " " + C("2", "→ " + (firstLine(res.out || res.err) || (res.ok ? "(ok)" : "(fehler)"))));
|
|
86
112
|
if (!res.ok) { say(" " + C("31", t("failed"))); return; }
|
|
87
113
|
}
|
|
88
114
|
if (a === "yes") await recordYes(check.id, { rl });
|
|
89
|
-
|
|
115
|
+
// the check that fixed something is run again for a fresh real line (the demo is not in the battery: it runs here)
|
|
116
|
+
const after = battery.some((c) => c.id === check.id)
|
|
117
|
+
? (await waitFor(t("waitCheck")(targetLabel()), job({ kind: "checks", ids: [check.id] }, target$()))).results[0] || brokeOff(check)
|
|
118
|
+
: check.run();
|
|
90
119
|
say(" " + C("2", t("recheck")));
|
|
91
120
|
say(renderResult(after).split("\n").map((l) => " " + l).join("\n"));
|
|
92
121
|
say(" " + (after.verdict === SAFE ? C("42;30", t("closed")) + C("2", t("closedSub")) : C("33", t("notYet"))));
|
|
@@ -122,9 +151,10 @@ export async function session() {
|
|
|
122
151
|
}
|
|
123
152
|
case "fingerabdruck": {
|
|
124
153
|
// "What is my system?" — answered from what the target itself prints, in five lines at most.
|
|
125
|
-
const
|
|
154
|
+
const o = await waitFor(t("waitLook")(targetLabel()), job({ kind: "exec", scripts: [OVERVIEW] }, target$()));
|
|
155
|
+
if (!o.reached) return unreached(o);
|
|
126
156
|
const kv = {};
|
|
127
|
-
for (const l of
|
|
157
|
+
for (const l of o.outs[0].out.split("\n")) { const i = l.indexOf("="); if (i > 0) kv[l.slice(0, i)] = l.slice(i + 1).trim(); }
|
|
128
158
|
const S = OV[lang()], fam = FAMILY_OF[(kv.ID || "").toLowerCase()] || "unbekannt";
|
|
129
159
|
say(" " + C("1", S.system) + " " + (kv.OS || (kv.MAC || "").trim() || kv.ID || "?") + (kv.KERNEL ? " · " + kv.KERNEL : "") + (kv.ARCH ? " · " + kv.ARCH : "") + C("2", " → " + ((kv.ID || "") + " " + (kv.VER || "")).trim()));
|
|
130
160
|
say(" " + (PKG[fam] ? C("1", S.pkg) + " " + PKG[fam] + C("2", ` (${S.family} ${fam})`) : C("1", S.pkg) + " " + S.pkgUnknown));
|
|
@@ -134,8 +164,9 @@ export async function session() {
|
|
|
134
164
|
return;
|
|
135
165
|
}
|
|
136
166
|
case "finde_php": {
|
|
137
|
-
const
|
|
138
|
-
|
|
167
|
+
const o = await waitFor(t("waitLook")(targetLabel()), job({ kind: "exec", scripts: ["php -v 2>/dev/null | head -1"] }, target$()));
|
|
168
|
+
if (!o.reached) return unreached(o);
|
|
169
|
+
const line = firstLine(o.outs[0].out);
|
|
139
170
|
say(line ? " " + C("1", SYS_T.php[lang()]) + C("2", " → " + line) : " " + C("2", SYS_T.phpNo[lang()]));
|
|
140
171
|
return;
|
|
141
172
|
}
|
|
@@ -165,8 +196,7 @@ export async function session() {
|
|
|
165
196
|
if (!readLicense()) { say(" " + C("2", p.noLicense)); return; }
|
|
166
197
|
say(" " + C("2", p.askFetch) + "\n " + C("2", curlShown()));
|
|
167
198
|
if (!(await yesNo(p.go))) { say(" " + C("2", p.declined)); return; }
|
|
168
|
-
|
|
169
|
-
const f = fetchPacks();
|
|
199
|
+
const f = await waitFor(p.loading, fetchPacksAsync());
|
|
170
200
|
if (!f.ok) { say(" " + C("33", failText(f))); return; }
|
|
171
201
|
showInstalled(installMany(f.raws));
|
|
172
202
|
};
|
|
@@ -222,7 +252,8 @@ export async function session() {
|
|
|
222
252
|
|
|
223
253
|
let pending = null; // the guess Dante asked back about; a plain "ja" takes it
|
|
224
254
|
for (;;) {
|
|
225
|
-
|
|
255
|
+
footerNext(); // a new tip with every input, like a loading screen
|
|
256
|
+
const line = (await rl.question("\n" + C("32;1", "dante ❯ "))).trim();
|
|
226
257
|
if (!line) continue;
|
|
227
258
|
const [first, ...rest] = line.split(/\s+/);
|
|
228
259
|
const arg = rest.join(" ");
|
|
@@ -238,7 +269,7 @@ export async function session() {
|
|
|
238
269
|
// A hard bolt, no probability: words of attack never reach a check, whatever the ear heard.
|
|
239
270
|
if (ATTACK.test(line) && !DEFEND.test(line)) { say(" " + C("2", t("notMine"))); continue; }
|
|
240
271
|
switch (heard.intent) {
|
|
241
|
-
case "raus": say(" " + C("2", t("bye"))); rl.close(); return;
|
|
272
|
+
case "raus": say(" " + C("2", t("bye"))); footerStop(); rl.close(); return;
|
|
242
273
|
case "gruss": say(" " + pick(t("hello"))); break;
|
|
243
274
|
case "hilfe": say(help()); break;
|
|
244
275
|
case "key": key = pfad || named(/^(key|schlüssel|llave|clave)$/i); configureTarget(target ? { host: target, key } : null); say(" " + C("2", t("key")) + (key || t("none"))); break;
|
|
@@ -253,6 +284,7 @@ export async function session() {
|
|
|
253
284
|
case "demo": { configureTarget(target ? { host: target, key } : null); run("touch /tmp/dante-demo-hole"); const r = demo.run(); say(renderResult(r)); if (r.fix) await finding(demo, r); break; }
|
|
254
285
|
case "smalltalk": say(pick(ANSWERS.smalltalk[lang()]).map((l) => " " + l).join("\n")); break;
|
|
255
286
|
case "frage_luecke": case "frage_urteil": case "frage_gehackt": case "frage_dante":
|
|
287
|
+
warmDomain();
|
|
256
288
|
// "zeig mir die zeile die das beweist" overlaps frage_urteil (what a Beleg IS). If a check
|
|
257
289
|
// already ran and the system ear is sure it is a beleg ask, show the real last line instead
|
|
258
290
|
// of explaining the concept.
|
|
@@ -270,6 +302,7 @@ export async function session() {
|
|
|
270
302
|
// The main ear has a gap → the domain ears (muster, system, wissen) get a turn. Each also knows
|
|
271
303
|
// what is not its own, and each is conservative (both neurons must be sure), so plain talk is
|
|
272
304
|
// never pulled into a patrol or an explanation.
|
|
305
|
+
warmDomain();
|
|
273
306
|
const dom = hearDomain(line);
|
|
274
307
|
if (dom) { if (dom.ear === "muster") await musterMove(dom.intent); else if (dom.ear === "system") await systemMove(dom.intent); else knowMove(dom.intent, line); break; }
|
|
275
308
|
// Not sure → ask back with the two best guesses. Outside his work → say so, plainly.
|
package/src/ticker.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// What the footer says: the GTA-loading-screen tips and Dante's inner clock. The clock is local arithmetic —
|
|
2
|
+
// Dante never phones home to ask for updates. Past STALE days it says so, and says how to get fresh knowledge.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
4
|
export const BIB_DATE = "2026-10-25"; // bump with every bib update
|
|
5
|
+
// The version of the package that is running — shown in the clock, so nobody has to guess.
|
|
6
|
+
export const VERSION = (() => { try { return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version || ""; } catch { return ""; } })();
|
|
5
7
|
export const STALE = 14; // days until Dante says his knowledge is getting old
|
|
6
8
|
// The date the clock counts from: the package's own knowledge, or a pack's issue date once one is active.
|
|
7
9
|
let knowledge = BIB_DATE;
|
|
8
10
|
export const setKnowledge = (iso) => { const d = String(iso || "").slice(0, 10); if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d > BIB_DATE) knowledge = d; };
|
|
9
11
|
|
|
10
|
-
const
|
|
11
|
-
const TIPS = {
|
|
12
|
+
export const TIPS = {
|
|
12
13
|
de: [
|
|
13
14
|
"Dante sagt nie „sicher“ ohne echte Zeile.", "Kein Ja, keine Änderung. Nie.",
|
|
14
|
-
"Dante
|
|
15
|
+
"Dante ruft von sich aus nie an. Nur „aktualisier“ nach deinem Ja holt Packs.",
|
|
15
16
|
"Eine Versionsnummer allein entscheidet nie — Distros flicken leise.",
|
|
16
17
|
"Sieht er dein Ziel nicht, sagt er „unbekannt“. Nicht „sicher“.",
|
|
17
18
|
"Sag einfach, was du willst: „prüf meinen server root@host“.",
|
|
@@ -20,7 +21,7 @@ const TIPS = {
|
|
|
20
21
|
],
|
|
21
22
|
en: [
|
|
22
23
|
"Dante never says „safe“ without a real line.", "No yes, no change. Ever.",
|
|
23
|
-
"Dante
|
|
24
|
+
"Dante never calls anyone on his own. Only „update“, after your yes, fetches packs.",
|
|
24
25
|
"A version number alone never decides — distros patch quietly.",
|
|
25
26
|
"If he cannot see your target, he says „unknown“. Not „safe“.",
|
|
26
27
|
"Just say what you want: „check my server root@host“.",
|
|
@@ -29,7 +30,7 @@ const TIPS = {
|
|
|
29
30
|
],
|
|
30
31
|
es: [
|
|
31
32
|
"Dante nunca dice „seguro“ sin una línea real.", "Sin sí, no hay cambio. Nunca.",
|
|
32
|
-
"Dante
|
|
33
|
+
"Dante nunca llama por su cuenta. Solo „actualiza“, tras tu sí, trae packs.",
|
|
33
34
|
"Un número de versión solo nunca decide — las distros parchean en silencio.",
|
|
34
35
|
"Si no ve tu destino, dice „desconocido“. No „seguro“.",
|
|
35
36
|
"Di lo que quieres: „revisa mi servidor root@host“.",
|
|
@@ -37,7 +38,7 @@ const TIPS = {
|
|
|
37
38
|
"Lo último: npx dante-watch@latest", "Reglas frescas en packs sueltos: escribe „actualiza“. Los viejos se quedan.",
|
|
38
39
|
],
|
|
39
40
|
};
|
|
40
|
-
const OLD = {
|
|
41
|
+
export const OLD = {
|
|
41
42
|
de: (d) => `Wissen ${d} Tage alt — neueste Updates: ${UPDATE} · frische Regeln: „aktualisier“`,
|
|
42
43
|
en: (d) => `Knowledge is ${d} days old — latest updates: ${UPDATE} · fresh rules: „update“`,
|
|
43
44
|
es: (d) => `Conocimiento de hace ${d} días — últimas novedades: ${UPDATE} · reglas frescas: „actualiza“`,
|
|
@@ -48,40 +49,16 @@ export function age(now = Date.now()) {
|
|
|
48
49
|
return Math.max(0, Math.floor((now - Date.parse(knowledge + "T00:00:00Z")) / 864e5));
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function frame(tick, tipIdx, shown, cols, lang) {
|
|
56
|
-
const days = age();
|
|
57
|
-
const stale = days > STALE;
|
|
58
|
-
const tip = stale && tipIdx % 2 ? OLD[lang](days) : TIPS[lang][tipIdx % TIPS[lang].length];
|
|
59
|
-
const clock = stale ? `bib ${days}d · !` : `bib ${knowledge.slice(8, 10)}.${knowledge.slice(5, 7)}. · ${days}d`;
|
|
60
|
-
const room = cols - 2 - 2 - 2 - clock.length - 2;
|
|
61
|
-
const text = cut(tip.slice(0, shown), room);
|
|
62
|
-
const gap = " ".repeat(Math.max(1, room - text.length + 2));
|
|
63
|
-
const tipC = stale && tipIdx % 2 ? "33" : "2";
|
|
64
|
-
return ` \x1b[36m${WHEEL[tick % WHEEL.length]}\x1b[0m \x1b[${tipC}m${text}\x1b[0m${gap}\x1b[${stale ? "33" : "2"}m${clock}\x1b[0m`;
|
|
52
|
+
// "bib 25.10. · 0d" — the date of the knowledge and how old it is; "bib 20d · !" once it is stale.
|
|
53
|
+
export function clockText() {
|
|
54
|
+
const days = age(), v = VERSION ? ` · v${VERSION}` : "";
|
|
55
|
+
return (days > STALE ? `bib ${days}d · !` : `bib ${knowledge.slice(8, 10)}.${knowledge.slice(5, 7)}. · ${days}d`) + v;
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
//
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
process.stdout.write("\n\x1b[1A"); // reserve the row below, so drawing there never scrolls
|
|
72
|
-
let tick = 0, tipIdx = Math.floor(Math.random() * TIPS[lang].length), shown = 0, hold = 0;
|
|
73
|
-
const draw = () => {
|
|
74
|
-
const cols = process.stdout.columns || 80;
|
|
75
|
-
process.stdout.write("\x1b7\x1b[1B\r\x1b[2K" + frame(tick, tipIdx, shown, cols, lang) + "\x1b8");
|
|
76
|
-
tick++;
|
|
77
|
-
const full = (age() > STALE && tipIdx % 2 ? 80 : TIPS[lang][tipIdx % TIPS[lang].length].length);
|
|
78
|
-
if (shown < full) shown += 2;
|
|
79
|
-
else if (++hold > 40) { hold = 0; shown = 0; tipIdx++; } // ~5 s per tip, then the next types in
|
|
80
|
-
};
|
|
81
|
-
const timer = setInterval(draw, 120);
|
|
82
|
-
draw();
|
|
83
|
-
try { return await rl.question(q); }
|
|
84
|
-
finally { clearInterval(timer); process.stdout.write("\r\x1b[2K"); } // Enter moved us onto the ticker row
|
|
58
|
+
// The tip for slot i in a language. Once the knowledge is stale, every second tip says so.
|
|
59
|
+
export function tipAt(i, lang = "de") {
|
|
60
|
+
const L = TIPS[lang] ? lang : "en", days = age();
|
|
61
|
+
return days > STALE && i % 2 ? OLD[L](days) : TIPS[L][i % TIPS[L].length];
|
|
85
62
|
}
|
|
86
63
|
|
|
87
64
|
// One line for one-shot runs (cron, --json excluded): only when the knowledge is old.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The footer — Virgil's, one to one: the two lowest lines of the terminal stay where they are (a scroll
|
|
2
|
+
// region, like the status bar of vim or tmux) and everything else scrolls above. A thin line, and under it a
|
|
3
|
+
// GTA-loading-screen tip that changes with every input. Dante adds his inner clock on the right.
|
|
4
|
+
// Only in a real terminal. The scroll region is ALWAYS reset on exit, or the terminal would stay frozen.
|
|
5
|
+
import { lang } from "../i18n.mjs";
|
|
6
|
+
import { tipAt, clockText, age, STALE } from "../ticker.mjs";
|
|
7
|
+
|
|
8
|
+
let on = false, timer = null, tipIdx = Math.floor(Math.random() * 8);
|
|
9
|
+
const rows = () => process.stdout.rows || 24, cols = () => process.stdout.columns || 80;
|
|
10
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`, yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
11
|
+
const cut = (s, n) => (s.length > n ? s.slice(0, Math.max(0, n - 1)) + "…" : s);
|
|
12
|
+
|
|
13
|
+
function draw() {
|
|
14
|
+
if (!on) return;
|
|
15
|
+
const r = rows(), c = cols(), clock = clockText();
|
|
16
|
+
const tip = cut(tipAt(tipIdx, lang()), Math.max(10, c - 4 - clock.length - 3));
|
|
17
|
+
const gap = " ".repeat(Math.max(1, c - 4 - tip.length - clock.length - 2));
|
|
18
|
+
const row = dim(" ◍ ") + yellow(tip) + gap + (age() > STALE ? yellow(clock) : dim(clock));
|
|
19
|
+
// save the cursor → the two reserved lines → clear + write → cursor back
|
|
20
|
+
process.stdout.write("\x1b7" + `\x1b[${r - 1};1H\x1b[2K` + dim("─".repeat(Math.max(10, c - 2))) + `\x1b[${r};1H\x1b[2K` + row + "\x1b8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function footerStart() {
|
|
24
|
+
if (!process.stdout.isTTY || process.env.DANTE_NO_FOOTER || on) return;
|
|
25
|
+
on = true;
|
|
26
|
+
const r = rows();
|
|
27
|
+
process.stdout.write(`\x1b[1;${r - 2}r`); // scroll region = the top, without the lowest 2 lines
|
|
28
|
+
process.stdout.write(`\x1b[${r - 2};1H`); // cursor into the upper part
|
|
29
|
+
draw();
|
|
30
|
+
// four times a second, so the footer stays visible even when readline or other output paints over it
|
|
31
|
+
timer = setInterval(draw, 250); timer.unref?.();
|
|
32
|
+
process.stdout.on("resize", draw);
|
|
33
|
+
process.on("exit", footerStop);
|
|
34
|
+
}
|
|
35
|
+
export function footerStop() {
|
|
36
|
+
if (!on) return;
|
|
37
|
+
on = false;
|
|
38
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
39
|
+
const r = rows();
|
|
40
|
+
process.stdout.write("\x1b[r"); // scroll region back to the whole screen
|
|
41
|
+
process.stdout.write(`\x1b[${r - 1};1H\x1b[2K\x1b[${r};1H\x1b[2K`);
|
|
42
|
+
try { process.stdout.off("resize", draw); } catch { /* fine */ }
|
|
43
|
+
}
|
|
44
|
+
/** The next tip — call before every input. */
|
|
45
|
+
export function footerNext() { if (on) { tipIdx++; draw(); } }
|
|
46
|
+
export const footerOn = () => on;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Runs in a worker thread: aim at the target, prove the channel, then run what was asked.
|
|
2
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
import { configureTarget, probe, run } from "../core/sh.mjs";
|
|
4
|
+
import { setLang } from "../i18n.mjs";
|
|
5
|
+
import { activeBattery, runCheck } from "../checks/battery.mjs";
|
|
6
|
+
|
|
7
|
+
const { task, target, lang } = workerData;
|
|
8
|
+
setLang(lang);
|
|
9
|
+
configureTarget(target);
|
|
10
|
+
const ch = probe();
|
|
11
|
+
if (!ch.reached) parentPort.postMessage({ reached: false, line: ch.line });
|
|
12
|
+
else if (task.kind === "checks") {
|
|
13
|
+
const { battery } = activeBattery();
|
|
14
|
+
const by = new Map(battery.map((c) => [c.id, c]));
|
|
15
|
+
parentPort.postMessage({ reached: true, results: task.ids.map((id) => (by.has(id) ? runCheck(by.get(id)) : null)) });
|
|
16
|
+
} else {
|
|
17
|
+
parentPort.postMessage({ reached: true, outs: task.scripts.map((s) => { const r = run(s, { timeout: task.timeout || 15000 }); return { out: r.out, err: r.err, ok: r.ok, ran: r.ran }; }) });
|
|
18
|
+
}
|
package/src/ui/job.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The checks block while they run: they call ssh and wait for the server (spawnSync). If that ran on the
|
|
2
|
+
// main thread, the waiting screen could not move. So the slow work runs in a worker thread and the main
|
|
3
|
+
// thread keeps the dinosaur running. The worker gets plain data in and gives plain data back; it loads the
|
|
4
|
+
// same built-in rules and packs as the session does.
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
6
|
+
import { lang } from "../i18n.mjs";
|
|
7
|
+
|
|
8
|
+
// task: { kind: "checks", ids } | { kind: "exec", scripts } target: { host, key } | null
|
|
9
|
+
export function job(task, target) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const w = new Worker(new URL("./job-worker.mjs", import.meta.url), { workerData: { task, target, lang: lang() }, execArgv: [] } /* no start options of the parent (a worker must not inherit --input-type & co) */);
|
|
12
|
+
w.once("message", resolve);
|
|
13
|
+
w.once("error", reject);
|
|
14
|
+
w.once("exit", (code) => { if (code !== 0) reject(new Error("worker exit " + code)); });
|
|
15
|
+
});
|
|
16
|
+
}
|
package/src/ui/quips.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// What Dante says while you wait. A terminal you stare at for a few seconds may as well say something.
|
|
2
|
+
// Same idea as Virgil's spinner sayings, in Dante's voice (the pilgrim, the map, the circles of Hell).
|
|
3
|
+
import { lang } from "../i18n.mjs";
|
|
4
|
+
|
|
5
|
+
const QUIPS = {
|
|
6
|
+
de: ["zählt die Kreise der Hölle …", "poliert das Schwert …", "liest Virgils Karte …", "fragt den Fährmann …", "sucht die Tür, die offen stand …", "schärft die Zeilen …", "trinkt einen Espresso …", "prüft die Papiere von Charon …", "wartet, bis der Hund aufhört zu bellen …", "geht noch einen Kreis tiefer …", "schaut hinter jede Tür …", "nimmt es nicht für sicher, bis die Zeile da ist …"],
|
|
7
|
+
en: ["counting the circles of Hell …", "polishing the sword …", "reading Virgil's map …", "asking the ferryman …", "looking for the door that stood open …", "sharpening the lines …", "having an espresso …", "checking Charon's papers …", "waiting for the dog to stop barking …", "going one circle deeper …", "looking behind every door …", "not calling it safe until the line is there …"],
|
|
8
|
+
es: ["contando los círculos del infierno …", "puliendo la espada …", "leyendo el mapa de Virgilio …", "preguntando al barquero …", "buscando la puerta que quedó abierta …", "afilando las líneas …", "tomando un espresso …", "revisando los papeles de Caronte …", "esperando a que el perro deje de ladrar …", "bajando un círculo más …", "mirando detrás de cada puerta …", "sin darlo por seguro hasta que esté la línea …"],
|
|
9
|
+
};
|
|
10
|
+
let at = Math.floor(Math.random() * 12), tick = 0;
|
|
11
|
+
/** A saying that changes every ~4 seconds of calls (each frame calls once), so a long wait does not repeat itself. */
|
|
12
|
+
export function quip() {
|
|
13
|
+
const xs = QUIPS[lang()] || QUIPS.en;
|
|
14
|
+
if (tick++ % 33 === 0) at = Math.floor(Math.random() * xs.length);
|
|
15
|
+
return xs[at % xs.length];
|
|
16
|
+
}
|
package/src/ui/wait.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The waiting screen — Virgil's, one to one: while a long step runs, one line shows a block swinging back
|
|
2
|
+
// and forth in a bar, a dinosaur running beside it, the name of the step and the seconds. Anything the program
|
|
3
|
+
// prints meanwhile goes past the line first (console.log is wrapped), so nothing is ever written over the bar.
|
|
4
|
+
// Only in a real terminal; DANTE_NO_WAIT=1 switches it off.
|
|
5
|
+
import { quip } from "./quips.mjs";
|
|
6
|
+
|
|
7
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`, cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
8
|
+
const stack = [];
|
|
9
|
+
let timer = null, frame = 0, shown = 0, suppressed = 0;
|
|
10
|
+
|
|
11
|
+
const live = () => (!!process.stdout.isTTY || process.env.DANTE_FORCE_WAIT === "1") && !process.env.DANTE_NO_WAIT;
|
|
12
|
+
const BAR = 16, BLOCK = 4;
|
|
13
|
+
|
|
14
|
+
function line() {
|
|
15
|
+
const top = stack[stack.length - 1];
|
|
16
|
+
const secs = Math.floor((Date.now() - top.t0) / 1000);
|
|
17
|
+
const period = (BAR - BLOCK) * 2, p = frame % period;
|
|
18
|
+
const at = p < BAR - BLOCK ? p : period - p; // the block swings to and fro
|
|
19
|
+
let bar = "";
|
|
20
|
+
for (let i = 0; i < BAR; i++) bar += i >= at && i < at + BLOCK ? "▓" : "░";
|
|
21
|
+
const dino = frame % 6 < 3 ? "🦖 " : " 🦖"; // runs, sniffs, runs
|
|
22
|
+
const depth = stack.length > 1 ? dim(" (" + stack.length + " steps)") : "";
|
|
23
|
+
const saying = secs >= 3 ? dim(" · " + quip()) : "";
|
|
24
|
+
return " " + cyan("▕" + bar + "▏") + " " + dino + " " + dim(top.label) + depth + dim(" · " + secs + "s") + saying;
|
|
25
|
+
}
|
|
26
|
+
const width = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length + 2; // an emoji is two columns wide
|
|
27
|
+
|
|
28
|
+
function clear() {
|
|
29
|
+
if (shown > 0) { process.stdout.write("\r\x1b[2K"); shown = 0; }
|
|
30
|
+
}
|
|
31
|
+
function draw() {
|
|
32
|
+
if (!live() || suppressed > 0 || !stack.length) return;
|
|
33
|
+
frame++;
|
|
34
|
+
const l = line();
|
|
35
|
+
clear();
|
|
36
|
+
process.stdout.write(l);
|
|
37
|
+
shown = width(l);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const orig = { log: console.log, error: console.error };
|
|
41
|
+
let patched = false;
|
|
42
|
+
function patchConsole() {
|
|
43
|
+
if (patched || !live()) return;
|
|
44
|
+
patched = true;
|
|
45
|
+
console.log = (...a) => { clear(); orig.log(...a); draw(); };
|
|
46
|
+
console.error = (...a) => { clear(); orig.error(...a); draw(); };
|
|
47
|
+
}
|
|
48
|
+
function unpatchConsole() {
|
|
49
|
+
if (!patched) return;
|
|
50
|
+
console.log = orig.log; console.error = orig.error; patched = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Begin a long step. May nest — the innermost one is shown. */
|
|
54
|
+
export function waitStart(label) {
|
|
55
|
+
stack.push({ label, t0: Date.now() });
|
|
56
|
+
patchConsole();
|
|
57
|
+
if (!timer && live()) { timer = setInterval(draw, 120); timer.unref?.(); }
|
|
58
|
+
draw();
|
|
59
|
+
}
|
|
60
|
+
/** End the innermost step. */
|
|
61
|
+
export function waitEnd() {
|
|
62
|
+
stack.pop();
|
|
63
|
+
if (!stack.length) {
|
|
64
|
+
if (timer) clearInterval(timer);
|
|
65
|
+
timer = null; clear(); unpatchConsole();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Do some work with the display — the line goes away by itself, even after an error. */
|
|
69
|
+
export async function waitFor(label, work) {
|
|
70
|
+
waitStart(label);
|
|
71
|
+
try { return await (typeof work === "function" ? work() : work); } finally { waitEnd(); }
|
|
72
|
+
}
|
|
73
|
+
/** Before any other output: line away. After it: draw again. */
|
|
74
|
+
export const waitHide = clear;
|
|
75
|
+
export const waitShow = draw;
|
|
76
|
+
/** A question needs the line: do not draw while it is asked. */
|
|
77
|
+
export function waitSuppress(on) {
|
|
78
|
+
suppressed = Math.max(0, suppressed + (on ? 1 : -1));
|
|
79
|
+
if (on) clear(); else draw();
|
|
80
|
+
}
|