pyre-cli 3.0.0 → 3.2.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 +98 -12
- package/dist/index.js +1221 -1021
- package/package.json +1 -1
- package/src/formatters/index.ts +1 -1
- package/src/formatters/render.ts +2 -1
- package/src/index.ts +2 -2
- package/src/live/input.ts +136 -3
- package/src/live/render.ts +70 -32
- package/src/live/state.ts +22 -15
- package/src/live/types.ts +1 -1
- package/src/p2p/server.ts +50 -13
- package/src/p2p/types.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -859,7 +859,8 @@ var TAB_DEFS = [
|
|
|
859
859
|
{ id: "packets", label: "Packets", key: "8" },
|
|
860
860
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
861
861
|
{ id: "disk", label: "Disk", key: "0" },
|
|
862
|
-
{ id: "process", label: "Process", key: "
|
|
862
|
+
{ id: "process", label: "Process", key: "P" },
|
|
863
|
+
{ id: "p2p", label: "P2P", key: "R" }
|
|
863
864
|
];
|
|
864
865
|
var TAB_BAR_INDENT = 2;
|
|
865
866
|
var TAB_SEPARATOR_WIDTH = 3;
|
|
@@ -1485,8 +1486,15 @@ var state = {
|
|
|
1485
1486
|
{ id: "packets", label: "Packets", key: "8" },
|
|
1486
1487
|
{ id: "tasks", label: "Tasks", key: "9" },
|
|
1487
1488
|
{ id: "disk", label: "Disk", key: "0" },
|
|
1488
|
-
{ id: "process", label: "Process", key: "P" }
|
|
1489
|
-
|
|
1489
|
+
{ id: "process", label: "Process", key: "P" },
|
|
1490
|
+
{ id: "p2p", label: "P2P", key: "R" }
|
|
1491
|
+
],
|
|
1492
|
+
p2pEvents: [],
|
|
1493
|
+
p2pBind: "",
|
|
1494
|
+
p2pServer: null,
|
|
1495
|
+
p2pServerRunning: false,
|
|
1496
|
+
p2pPort: 9876,
|
|
1497
|
+
p2pPassword: "pyre"
|
|
1490
1498
|
};
|
|
1491
1499
|
function setStatus(msg, ms = 3e3) {
|
|
1492
1500
|
state.statusMessage = msg;
|
|
@@ -1563,6 +1571,29 @@ function processRowBudget() {
|
|
|
1563
1571
|
const reserved = 2 + cardRows * 8 + 11 + (state.showGraphs ? 9 : 0) + 4;
|
|
1564
1572
|
return Math.max(3, Math.min(40, state.termHeight - reserved));
|
|
1565
1573
|
}
|
|
1574
|
+
function p2pPanelLines() {
|
|
1575
|
+
const events = state.p2pServer ? state.p2pServer.peerEventHistory : state.p2pEvents;
|
|
1576
|
+
const body = [];
|
|
1577
|
+
const bindAddr = state.p2pBind || `0.0.0.0:${state.p2pPort}`;
|
|
1578
|
+
body.push(`Status: ${state.p2pServerRunning ? chalk5.green("Running") : chalk5.red("Stopped")}`);
|
|
1579
|
+
body.push(`Bind: ${bindAddr}`);
|
|
1580
|
+
body.push(`Password: ${state.p2pPassword}`);
|
|
1581
|
+
body.push("");
|
|
1582
|
+
const typeColor = (t) => {
|
|
1583
|
+
if (t === "auth") return chalk5.green;
|
|
1584
|
+
if (t === "disconnect") return chalk5.yellow;
|
|
1585
|
+
if (t === "error" || t === "blocked" || t === "rate_limited") return chalk5.red;
|
|
1586
|
+
return chalk5.cyan;
|
|
1587
|
+
};
|
|
1588
|
+
body.push(chalk5.dim("Events (most recent):"));
|
|
1589
|
+
const recent = events.slice(-20).reverse();
|
|
1590
|
+
for (const evt of recent) {
|
|
1591
|
+
const time = new Date(evt.ts).toLocaleTimeString();
|
|
1592
|
+
body.push(`${chalk5.dim(time)} ${typeColor(evt.type)(evt.type.padEnd(14))} ${evt.detail}`);
|
|
1593
|
+
}
|
|
1594
|
+
const title = `P2P \xB7 ${state.p2pServerRunning ? "ON" : "OFF"} \xB7 ${events.length} events`;
|
|
1595
|
+
return { title, body };
|
|
1596
|
+
}
|
|
1566
1597
|
function renderCustomizerOverlay() {
|
|
1567
1598
|
const themesList = Object.keys(THEMES);
|
|
1568
1599
|
const lines = [
|
|
@@ -1590,19 +1621,25 @@ function renderCustomizerOverlay() {
|
|
|
1590
1621
|
function render() {
|
|
1591
1622
|
if (!state.lastData) return;
|
|
1592
1623
|
const lines = [];
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1624
|
+
const theme = THEMES[state.currentTheme] || THEMES.default;
|
|
1625
|
+
if (state.activePanel === "p2p") {
|
|
1626
|
+
const panelLines = p2pPanelLines();
|
|
1627
|
+
lines.push(...panel(panelLines.title, panelLines.body, state.termWidth, theme.process, theme.border));
|
|
1628
|
+
} else {
|
|
1629
|
+
lines.push(
|
|
1630
|
+
formatTable(state.lastData, {
|
|
1631
|
+
width: state.termWidth,
|
|
1632
|
+
sortBy: state.sortMode,
|
|
1633
|
+
filter: state.processFilter || void 0,
|
|
1634
|
+
processLimit: processRowBudget(),
|
|
1635
|
+
theme: state.currentTheme,
|
|
1636
|
+
visible: state.visiblePanels,
|
|
1637
|
+
treeView: state.treeView,
|
|
1638
|
+
activePanel: state.activePanel
|
|
1639
|
+
})
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
if (state.showGraphs && state.activePanel !== "p2p") {
|
|
1606
1643
|
lines.push("");
|
|
1607
1644
|
lines.push(formatGraphs(state.history, state.termWidth, state.currentTheme, state.graphMode));
|
|
1608
1645
|
}
|
|
@@ -1620,6 +1657,8 @@ function render() {
|
|
|
1620
1657
|
lines.push(chalk5.cyan(` Signal: ${state.inputBuffer}_`));
|
|
1621
1658
|
lines.push(chalk5.dim(` ${sigList}`));
|
|
1622
1659
|
lines.push(chalk5.dim(" \u2191/\u2193 to select, Enter to confirm, Esc to cancel"));
|
|
1660
|
+
} else if (state.inputMode === "p2p") {
|
|
1661
|
+
lines.push(chalk5.cyan(` P2P password (${state.p2pPort}): ${state.inputBuffer}_ (enter to start, esc to cancel)`));
|
|
1623
1662
|
} else if (state.statusMessage) {
|
|
1624
1663
|
lines.push(` ${state.statusMessage}`);
|
|
1625
1664
|
}
|
|
@@ -1630,6 +1669,7 @@ function footerLine() {
|
|
|
1630
1669
|
const keys = [
|
|
1631
1670
|
["q", "quit"],
|
|
1632
1671
|
["c", "customize UI"],
|
|
1672
|
+
["r", state.p2pServerRunning ? "stop p2p" : "start p2p"],
|
|
1633
1673
|
["p", state.paused ? "resume" : "pause"],
|
|
1634
1674
|
["g", state.showGraphs ? "hide graphs" : "show graphs"],
|
|
1635
1675
|
["b", `graph:${state.graphMode}`],
|
|
@@ -1651,6 +1691,7 @@ function footerLine() {
|
|
|
1651
1691
|
if (state.paused) badges.push(chalk5.yellow.bold("\u23F8 PAUSED"));
|
|
1652
1692
|
if (state.logging) badges.push(chalk5.red.bold("\u25CF REC"));
|
|
1653
1693
|
if (state.activePanel !== "grid") badges.push(chalk5.cyan.bold(`\u25C9 ${state.activePanel.toUpperCase()}`));
|
|
1694
|
+
if (state.p2pServerRunning) badges.push(chalk5.green.bold(`P2P ${state.p2pBind || "0.0.0.0"}:${state.p2pPort}`));
|
|
1654
1695
|
const badgeStr = badges.join(" ");
|
|
1655
1696
|
return badgeStr ? `${keyStr} ${badgeStr}` : keyStr;
|
|
1656
1697
|
}
|
|
@@ -1667,1111 +1708,1270 @@ function checkAlerts(data) {
|
|
|
1667
1708
|
}
|
|
1668
1709
|
}
|
|
1669
1710
|
|
|
1670
|
-
// src/
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1711
|
+
// src/p2p/server.ts
|
|
1712
|
+
import net from "net";
|
|
1713
|
+
import tls from "tls";
|
|
1714
|
+
import fs2 from "fs";
|
|
1715
|
+
import crypto2 from "crypto";
|
|
1716
|
+
import chalk6 from "chalk";
|
|
1717
|
+
|
|
1718
|
+
// src/p2p/protocol.ts
|
|
1719
|
+
import crypto from "crypto";
|
|
1720
|
+
var HEADER_SIZE = 4;
|
|
1721
|
+
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
1722
|
+
var HMAC_ALGO = "sha256";
|
|
1723
|
+
function buildMessage(kind, payload, hmac) {
|
|
1724
|
+
const envelope = { kind, payload };
|
|
1725
|
+
if (hmac) envelope.hmac = hmac;
|
|
1726
|
+
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
1727
|
+
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
1728
|
+
header2.writeUInt32BE(body.length, 0);
|
|
1729
|
+
return Buffer.concat([header2, body]);
|
|
1730
|
+
}
|
|
1731
|
+
function generateNonce() {
|
|
1732
|
+
return crypto.randomBytes(32).toString("hex");
|
|
1733
|
+
}
|
|
1734
|
+
function hashPassword(password, nonce) {
|
|
1735
|
+
return crypto.createHash("sha256").update(`${password}:${nonce}`).digest("hex");
|
|
1736
|
+
}
|
|
1737
|
+
function computeHMAC(body, key) {
|
|
1738
|
+
if (!key) return void 0;
|
|
1739
|
+
return crypto.createHmac(HMAC_ALGO, key).update(body).digest("hex");
|
|
1740
|
+
}
|
|
1741
|
+
function verifyHMAC(body, expectedHmac, key) {
|
|
1742
|
+
if (!key) return expectedHmac === void 0;
|
|
1743
|
+
if (!expectedHmac) return false;
|
|
1744
|
+
try {
|
|
1745
|
+
const parsed = JSON.parse(body);
|
|
1746
|
+
const payloadWithoutHmac = { ...parsed, hmac: void 0 };
|
|
1747
|
+
const canonical = JSON.stringify(payloadWithoutHmac);
|
|
1748
|
+
const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest("hex");
|
|
1749
|
+
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
|
|
1750
|
+
} catch {
|
|
1751
|
+
return false;
|
|
1677
1752
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1753
|
+
}
|
|
1754
|
+
function encodeDisconnect(reason) {
|
|
1755
|
+
return buildMessage("disconnect", { reason });
|
|
1756
|
+
}
|
|
1757
|
+
function decodeMessage(buf) {
|
|
1758
|
+
const str = buf.toString("utf-8");
|
|
1759
|
+
const msg = JSON.parse(str);
|
|
1760
|
+
return msg;
|
|
1761
|
+
}
|
|
1762
|
+
function signMessage(kind, payload, key) {
|
|
1763
|
+
const envelope = { kind, payload };
|
|
1764
|
+
const bodyJson = JSON.stringify(envelope);
|
|
1765
|
+
const hmac = computeHMAC(bodyJson, key);
|
|
1766
|
+
return buildMessage(kind, payload, hmac);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/p2p/server.ts
|
|
1770
|
+
var HEADER_SIZE2 = 4;
|
|
1771
|
+
var DEFAULT_RATE_LIMIT_MAX = 5;
|
|
1772
|
+
var DEFAULT_RATE_LIMIT_WINDOW = 6e4;
|
|
1773
|
+
var HMAC_KEY_LENGTH = 32;
|
|
1774
|
+
function readMessageFromPeer(peer) {
|
|
1775
|
+
while (peer.readBuffer.length > 0) {
|
|
1776
|
+
if (peer.readExpectingHeader) {
|
|
1777
|
+
if (peer.readBuffer.length >= HEADER_SIZE2) {
|
|
1778
|
+
peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
|
|
1779
|
+
peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
|
|
1780
|
+
peer.readExpectingHeader = false;
|
|
1693
1781
|
} else {
|
|
1694
|
-
|
|
1695
|
-
state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
|
|
1782
|
+
break;
|
|
1696
1783
|
}
|
|
1697
1784
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
|
|
1708
|
-
} else if (key.name === "return" || str === " ") {
|
|
1709
|
-
killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
|
|
1710
|
-
state.inputMode = null;
|
|
1711
|
-
state.inputBuffer = "";
|
|
1712
|
-
render();
|
|
1713
|
-
return;
|
|
1785
|
+
if (!peer.readExpectingHeader) {
|
|
1786
|
+
if (peer.readBuffer.length >= peer.readMessageLength) {
|
|
1787
|
+
const body = peer.readBuffer.subarray(0, peer.readMessageLength);
|
|
1788
|
+
peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
|
|
1789
|
+
peer.readExpectingHeader = true;
|
|
1790
|
+
return body;
|
|
1791
|
+
} else {
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1714
1794
|
}
|
|
1715
|
-
render();
|
|
1716
|
-
return;
|
|
1717
1795
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
function isIPAllowed(ip, allowedIPs, deniedIPs) {
|
|
1799
|
+
for (const denied of deniedIPs) {
|
|
1800
|
+
if (ip === denied || ip.startsWith(denied)) {
|
|
1801
|
+
return false;
|
|
1724
1802
|
}
|
|
1725
|
-
state.inputMode = null;
|
|
1726
|
-
state.inputBuffer = "";
|
|
1727
|
-
render();
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
if (key.name === "backspace") {
|
|
1731
|
-
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
1732
|
-
render();
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
1736
|
-
state.inputBuffer += str;
|
|
1737
|
-
render();
|
|
1738
1803
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const pid = parseInt(pidStr, 10);
|
|
1742
|
-
if (!pid || pid <= 0) {
|
|
1743
|
-
setStatus(`Invalid PID: ${pidStr}`);
|
|
1744
|
-
return;
|
|
1804
|
+
if (allowedIPs.length === 0) {
|
|
1805
|
+
return true;
|
|
1745
1806
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
|
|
1807
|
+
for (const allowed of allowedIPs) {
|
|
1808
|
+
if (ip === allowed || ip.startsWith(allowed)) {
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1751
1811
|
}
|
|
1812
|
+
return false;
|
|
1752
1813
|
}
|
|
1753
|
-
function
|
|
1754
|
-
|
|
1755
|
-
state.termHeight = process.stdout.rows || 24;
|
|
1756
|
-
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
1757
|
-
render();
|
|
1758
|
-
}
|
|
1759
|
-
function restartTicker() {
|
|
1760
|
-
if (state.intervalHandle) clearInterval(state.intervalHandle);
|
|
1761
|
-
state.intervalHandle = setInterval(tick, state.interval * 1e3);
|
|
1814
|
+
function deriveHMACKey(password) {
|
|
1815
|
+
return crypto2.createHash("sha256").update(password).digest("hex").slice(0, HMAC_KEY_LENGTH);
|
|
1762
1816
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
|
|
1777
|
-
rxPackets: data.network.rxPackets,
|
|
1778
|
-
txPackets: data.network.txPackets,
|
|
1779
|
-
connections: data.network.connections ?? 0
|
|
1780
|
-
});
|
|
1781
|
-
writeLogRow(data);
|
|
1782
|
-
checkAlerts(data);
|
|
1783
|
-
render();
|
|
1784
|
-
} catch {
|
|
1817
|
+
var P2PServer = class {
|
|
1818
|
+
server = null;
|
|
1819
|
+
peers = /* @__PURE__ */ new Map();
|
|
1820
|
+
options;
|
|
1821
|
+
running = false;
|
|
1822
|
+
rateLimitMap = /* @__PURE__ */ new Map();
|
|
1823
|
+
auditStream = null;
|
|
1824
|
+
hmacKey;
|
|
1825
|
+
peerEvents = [];
|
|
1826
|
+
boundAddress = "";
|
|
1827
|
+
constructor(options) {
|
|
1828
|
+
this.options = options;
|
|
1829
|
+
this.hmacKey = deriveHMACKey(options.password);
|
|
1785
1830
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
if (state.running) return;
|
|
1789
|
-
state.running = true;
|
|
1790
|
-
state.paused = false;
|
|
1791
|
-
state.detailed = !!opts2.detailed;
|
|
1792
|
-
state.interval = opts2.interval;
|
|
1793
|
-
if (opts2.exportDir) state.exportDir = opts2.exportDir;
|
|
1794
|
-
state.termWidth = process.stdout.columns || 80;
|
|
1795
|
-
state.termHeight = process.stdout.rows || 24;
|
|
1796
|
-
state.history.reset();
|
|
1797
|
-
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
1798
|
-
process.stdout.write("\x1B[?1049h");
|
|
1799
|
-
process.stdout.write("\x1B[?25l");
|
|
1800
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
1801
|
-
process.title = "pyre";
|
|
1802
|
-
if (state.mouseEnabled) {
|
|
1803
|
-
process.stdout.write("\x1B[?1000h");
|
|
1831
|
+
get peerEventHistory() {
|
|
1832
|
+
return this.peerEvents;
|
|
1804
1833
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
process.stdin.resume();
|
|
1811
|
-
process.stdout.on("resize", onResize);
|
|
1812
|
-
state.keypressHandler = (str, key) => {
|
|
1813
|
-
if (!key) return;
|
|
1814
|
-
if (key.ctrl && key.name === "c") {
|
|
1815
|
-
stopLive();
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
if (state.inputMode) {
|
|
1819
|
-
handleInputModeKey(str, key);
|
|
1820
|
-
return;
|
|
1834
|
+
log(msg) {
|
|
1835
|
+
if (this.options.onLog) {
|
|
1836
|
+
this.options.onLog(msg.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
1837
|
+
} else {
|
|
1838
|
+
console.log(msg);
|
|
1821
1839
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1840
|
+
}
|
|
1841
|
+
emitPeerEvent(type, detail) {
|
|
1842
|
+
const evt = { type, detail, ts: Date.now() };
|
|
1843
|
+
this.peerEvents.push(evt);
|
|
1844
|
+
if (this.peerEvents.length > 200) this.peerEvents.shift();
|
|
1845
|
+
if (this.options.onPeerEvent) {
|
|
1846
|
+
this.options.onPeerEvent(evt);
|
|
1825
1847
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1848
|
+
}
|
|
1849
|
+
get info() {
|
|
1850
|
+
return {
|
|
1851
|
+
host: this.options.host,
|
|
1852
|
+
port: this.options.port,
|
|
1853
|
+
peersConnected: this.peers.size
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
async start() {
|
|
1857
|
+
if (this.options.auditLog) {
|
|
1858
|
+
try {
|
|
1859
|
+
fs2.mkdirSync(this.options.auditLog, { recursive: true });
|
|
1860
|
+
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
1861
|
+
this.auditStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
1862
|
+
} catch {
|
|
1863
|
+
this.log(chalk6.yellow("Warning: could not open audit log"));
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return new Promise((resolve, reject) => {
|
|
1867
|
+
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
1868
|
+
if (useTLS) {
|
|
1869
|
+
const tlsOptions = {
|
|
1870
|
+
key: fs2.readFileSync(this.options.tlsKey),
|
|
1871
|
+
cert: fs2.readFileSync(this.options.tlsCert)
|
|
1872
|
+
};
|
|
1873
|
+
if (this.options.tlsCA) {
|
|
1874
|
+
tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
|
|
1875
|
+
}
|
|
1876
|
+
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
1877
|
+
} else {
|
|
1878
|
+
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
1879
|
+
}
|
|
1880
|
+
this.server.on("error", (err) => reject(err));
|
|
1881
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
1882
|
+
this.running = true;
|
|
1883
|
+
const addr = this.server?.address();
|
|
1884
|
+
const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
|
|
1885
|
+
this.boundAddress = typeof addr === "object" && addr ? addr.address : String(addr);
|
|
1886
|
+
const protocol = useTLS ? "TLS" : "TCP";
|
|
1887
|
+
this.log(chalk6.green(`P2P server listening on ${bound} (${protocol})`));
|
|
1888
|
+
this.log(chalk6.dim(` Password: ${this.options.password}`));
|
|
1889
|
+
this.log(chalk6.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
1890
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
1891
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
1892
|
+
if (allowedIPs.length > 0) this.log(chalk6.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
|
|
1893
|
+
if (deniedIPs.length > 0) this.log(chalk6.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
|
|
1894
|
+
this.emitPeerEvent("connect", `Server listening on ${bound}`);
|
|
1895
|
+
resolve();
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
stop() {
|
|
1900
|
+
this.running = false;
|
|
1901
|
+
for (const peer of this.peers.values()) {
|
|
1902
|
+
this.disconnectPeer(peer, "Server shutting down");
|
|
1903
|
+
}
|
|
1904
|
+
this.peers.clear();
|
|
1905
|
+
this.rateLimitMap.clear();
|
|
1906
|
+
this.peerEvents = [];
|
|
1907
|
+
if (this.server) {
|
|
1908
|
+
this.server.close();
|
|
1909
|
+
this.server = null;
|
|
1910
|
+
}
|
|
1911
|
+
if (this.auditStream) {
|
|
1912
|
+
this.auditStream.end();
|
|
1913
|
+
this.auditStream = null;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
audit(ip, event, detail) {
|
|
1917
|
+
if (!this.auditStream) return;
|
|
1918
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1919
|
+
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
1920
|
+
`);
|
|
1921
|
+
}
|
|
1922
|
+
getPeerIP(socket) {
|
|
1923
|
+
const addr = socket.remoteAddress;
|
|
1924
|
+
if (!addr) return "unknown";
|
|
1925
|
+
if (addr.startsWith("::ffff:")) {
|
|
1926
|
+
return addr.slice(7);
|
|
1927
|
+
}
|
|
1928
|
+
return addr;
|
|
1929
|
+
}
|
|
1930
|
+
checkRateLimit(ip) {
|
|
1931
|
+
const maxAttempts = this.options.rateLimitMaxAttempts ?? DEFAULT_RATE_LIMIT_MAX;
|
|
1932
|
+
const windowMs = this.options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW;
|
|
1933
|
+
const now = Date.now();
|
|
1934
|
+
const entry = this.rateLimitMap.get(ip);
|
|
1935
|
+
if (!entry) {
|
|
1936
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
1937
|
+
return true;
|
|
1938
|
+
}
|
|
1939
|
+
if (now - entry.firstAttempt > windowMs) {
|
|
1940
|
+
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
1941
|
+
return true;
|
|
1942
|
+
}
|
|
1943
|
+
entry.attempts++;
|
|
1944
|
+
if (entry.attempts > maxAttempts) {
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
cleanupRateLimit(ip) {
|
|
1950
|
+
this.rateLimitMap.delete(ip);
|
|
1951
|
+
}
|
|
1952
|
+
onConnection(socket) {
|
|
1953
|
+
const ip = this.getPeerIP(socket);
|
|
1954
|
+
const allowedIPs = this.options.allowedIPs ?? [];
|
|
1955
|
+
const deniedIPs = this.options.deniedIPs ?? [];
|
|
1956
|
+
if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
|
|
1957
|
+
this.audit(ip, "BLOCKED", "IP not allowed");
|
|
1958
|
+
this.log(chalk6.yellow(`Blocked IP: ${ip}`));
|
|
1959
|
+
this.emitPeerEvent("blocked", ip);
|
|
1960
|
+
socket.destroy();
|
|
1828
1961
|
return;
|
|
1829
1962
|
}
|
|
1830
|
-
if (
|
|
1831
|
-
|
|
1963
|
+
if (!this.checkRateLimit(ip)) {
|
|
1964
|
+
this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
|
|
1965
|
+
this.log(chalk6.yellow(`Rate limit exceeded for IP: ${ip}`));
|
|
1966
|
+
this.emitPeerEvent("rate_limited", ip);
|
|
1967
|
+
socket.destroy();
|
|
1832
1968
|
return;
|
|
1833
1969
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1970
|
+
const peer = {
|
|
1971
|
+
socket,
|
|
1972
|
+
authenticated: false,
|
|
1973
|
+
intervalHandle: null,
|
|
1974
|
+
pingHandle: null,
|
|
1975
|
+
readBuffer: Buffer.alloc(0),
|
|
1976
|
+
readExpectingHeader: true,
|
|
1977
|
+
readMessageLength: 0,
|
|
1978
|
+
ip,
|
|
1979
|
+
pendingNonce: null
|
|
1980
|
+
};
|
|
1981
|
+
this.peers.set(socket, peer);
|
|
1982
|
+
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
1983
|
+
this.log(chalk6.cyan(`Peer connected: ${remoteAddr}`));
|
|
1984
|
+
this.audit(ip, "CONNECT", remoteAddr);
|
|
1985
|
+
this.emitPeerEvent("connect", remoteAddr);
|
|
1986
|
+
socket.on("data", (data) => this.onData(peer, data));
|
|
1987
|
+
socket.on("close", () => this.onPeerClose(peer));
|
|
1988
|
+
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
1989
|
+
socket.setTimeout(3e4);
|
|
1990
|
+
socket.on("timeout", () => {
|
|
1991
|
+
if (!peer.authenticated) {
|
|
1992
|
+
this.disconnectPeer(peer, "Authentication timeout");
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
const nonce = generateNonce();
|
|
1996
|
+
this.sendSigned(peer, signMessage("challenge", { nonce }, this.hmacKey));
|
|
1997
|
+
peer.pendingNonce = nonce;
|
|
1998
|
+
}
|
|
1999
|
+
onData(peer, data) {
|
|
2000
|
+
peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
|
|
2001
|
+
if (!peer.authenticated) {
|
|
2002
|
+
const body = readMessageFromPeer(peer);
|
|
2003
|
+
if (body) {
|
|
2004
|
+
this.handleAuthMessage(peer, body);
|
|
2005
|
+
}
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
handleAuthMessage(peer, body) {
|
|
2010
|
+
try {
|
|
2011
|
+
const raw = body.toString("utf-8");
|
|
2012
|
+
const msg = decodeMessage(body);
|
|
2013
|
+
if (msg.kind !== "auth") {
|
|
2014
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
|
|
2015
|
+
this.disconnectPeer(peer, "Expected auth message");
|
|
2016
|
+
this.emitPeerEvent("error", `Expected auth message from ${peer.ip}`);
|
|
1844
2017
|
return;
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2018
|
+
}
|
|
2019
|
+
const authPayload = msg.payload;
|
|
2020
|
+
if (!authPayload.passwordHash || !authPayload.nonce) {
|
|
2021
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
|
|
2022
|
+
this.disconnectPeer(peer, "Invalid auth payload");
|
|
2023
|
+
this.emitPeerEvent("error", `Invalid auth payload from ${peer.ip}`);
|
|
1849
2024
|
return;
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
case "6":
|
|
1857
|
-
case "7":
|
|
1858
|
-
case "8":
|
|
1859
|
-
case "9": {
|
|
1860
|
-
const tabMap = {
|
|
1861
|
-
"0": "disk",
|
|
1862
|
-
"1": "cpu",
|
|
1863
|
-
"2": "mem",
|
|
1864
|
-
"3": "gpu",
|
|
1865
|
-
"4": "power",
|
|
1866
|
-
"5": "battery",
|
|
1867
|
-
"6": "thermal",
|
|
1868
|
-
"7": "network",
|
|
1869
|
-
"8": "packets",
|
|
1870
|
-
"9": "tasks"
|
|
1871
|
-
};
|
|
1872
|
-
const panelId = tabMap[key.sequence];
|
|
1873
|
-
if (panelId) {
|
|
1874
|
-
state.activePanel = state.activePanel === panelId ? "grid" : panelId;
|
|
1875
|
-
setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
|
|
1876
|
-
render();
|
|
1877
|
-
}
|
|
2025
|
+
}
|
|
2026
|
+
if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
|
|
2027
|
+
this.audit(peer.ip, "AUTH_FAIL", "Nonce mismatch");
|
|
2028
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
|
|
2029
|
+
this.disconnectPeer(peer, "Invalid nonce");
|
|
2030
|
+
this.emitPeerEvent("error", `Invalid nonce from ${peer.ip}`);
|
|
1878
2031
|
return;
|
|
1879
2032
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2033
|
+
const expectedHash = hashPassword(this.options.password, authPayload.nonce);
|
|
2034
|
+
if (authPayload.passwordHash !== expectedHash) {
|
|
2035
|
+
this.audit(peer.ip, "AUTH_FAIL", "Invalid password hash");
|
|
2036
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
|
|
2037
|
+
this.disconnectPeer(peer, "Authentication failed");
|
|
2038
|
+
this.emitPeerEvent("error", `Invalid password from ${peer.ip}`);
|
|
1884
2039
|
return;
|
|
1885
2040
|
}
|
|
2041
|
+
peer.authenticated = true;
|
|
2042
|
+
this.cleanupRateLimit(peer.ip);
|
|
2043
|
+
peer.pendingNonce = null;
|
|
2044
|
+
peer.socket.write(signMessage("auth-ok", { ok: true }, this.hmacKey));
|
|
2045
|
+
this.log(chalk6.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2046
|
+
this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
|
|
2047
|
+
this.emitPeerEvent("auth", peer.socket.remoteAddress ?? peer.ip);
|
|
2048
|
+
this.startDataStream(peer);
|
|
2049
|
+
this.startPing(peer);
|
|
2050
|
+
} catch {
|
|
2051
|
+
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
|
|
2052
|
+
this.disconnectPeer(peer, "Invalid message format");
|
|
1886
2053
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
break;
|
|
2054
|
+
}
|
|
2055
|
+
sendSigned(peer, message) {
|
|
2056
|
+
if (!peer.socket.destroyed) {
|
|
2057
|
+
peer.socket.write(message);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
startDataStream(peer) {
|
|
2061
|
+
const send = async () => {
|
|
2062
|
+
if (!peer.authenticated || peer.socket.destroyed) return;
|
|
2063
|
+
try {
|
|
2064
|
+
const data = await collectAll({ detailed: this.options.detailed });
|
|
2065
|
+
this.sendSigned(peer, signMessage("data", { stats: data }, this.hmacKey));
|
|
2066
|
+
} catch {
|
|
1901
2067
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
1929
|
-
setStatus(`Graph mode: ${state.graphMode}`);
|
|
1930
|
-
render();
|
|
1931
|
-
break;
|
|
1932
|
-
case "d":
|
|
1933
|
-
state.detailed = !state.detailed;
|
|
1934
|
-
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
1935
|
-
break;
|
|
1936
|
-
case "s":
|
|
1937
|
-
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
1938
|
-
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
1939
|
-
state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
|
|
1940
|
-
setStatus(`Sorting by ${state.sortMode}`);
|
|
1941
|
-
render();
|
|
1942
|
-
break;
|
|
1943
|
-
case "k":
|
|
1944
|
-
state.inputMode = "kill";
|
|
1945
|
-
state.inputBuffer = "";
|
|
1946
|
-
render();
|
|
1947
|
-
break;
|
|
1948
|
-
case "S":
|
|
1949
|
-
state.inputMode = "signal";
|
|
1950
|
-
state.inputBuffer = "SIGTERM";
|
|
1951
|
-
render();
|
|
1952
|
-
break;
|
|
1953
|
-
case "t":
|
|
1954
|
-
state.treeView = !state.treeView;
|
|
1955
|
-
setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
|
|
1956
|
-
render();
|
|
1957
|
-
break;
|
|
1958
|
-
case "e":
|
|
1959
|
-
exportSnapshot();
|
|
1960
|
-
render();
|
|
1961
|
-
break;
|
|
1962
|
-
case "l":
|
|
1963
|
-
toggleLogging();
|
|
1964
|
-
render();
|
|
1965
|
-
break;
|
|
1966
|
-
case "f":
|
|
1967
|
-
state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
|
|
1968
|
-
setStatus(`Export format: ${state.exportFormat}`);
|
|
1969
|
-
render();
|
|
1970
|
-
break;
|
|
1971
|
-
case "tab":
|
|
1972
|
-
cycleTab(1);
|
|
1973
|
-
break;
|
|
1974
|
-
case "left":
|
|
1975
|
-
cycleTab(-1);
|
|
1976
|
-
break;
|
|
1977
|
-
case "right":
|
|
1978
|
-
cycleTab(1);
|
|
1979
|
-
break;
|
|
1980
|
-
default:
|
|
1981
|
-
if (str && str.length === 1) {
|
|
1982
|
-
const tabId = tabKeyToId(str);
|
|
1983
|
-
if (tabId) {
|
|
1984
|
-
state.activePanel = tabId;
|
|
1985
|
-
setStatus(`Panel: ${tabId.toUpperCase()}`);
|
|
1986
|
-
render();
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
2068
|
+
};
|
|
2069
|
+
send();
|
|
2070
|
+
peer.intervalHandle = setInterval(send, this.options.intervalMs);
|
|
2071
|
+
}
|
|
2072
|
+
startPing(peer) {
|
|
2073
|
+
peer.pingHandle = setInterval(() => {
|
|
2074
|
+
if (peer.socket.destroyed) return;
|
|
2075
|
+
this.sendSigned(peer, signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2076
|
+
}, 5e3);
|
|
2077
|
+
}
|
|
2078
|
+
onPeerClose(peer) {
|
|
2079
|
+
this.cleanupPeer(peer);
|
|
2080
|
+
this.log(chalk6.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
|
|
2081
|
+
this.emitPeerEvent("disconnect", peer.socket.remoteAddress ?? peer.ip);
|
|
2082
|
+
this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
|
|
2083
|
+
}
|
|
2084
|
+
onPeerError(peer, err) {
|
|
2085
|
+
const code = err.code;
|
|
2086
|
+
if (code === "ECONNRESET") {
|
|
2087
|
+
this.log(chalk6.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
|
|
2088
|
+
this.audit(peer.ip, "ERROR", "Connection reset");
|
|
2089
|
+
this.emitPeerEvent("error", `connection reset: ${peer.socket.remoteAddress}`);
|
|
2090
|
+
} else {
|
|
2091
|
+
this.log(chalk6.red(`Peer error: ${err.message}`));
|
|
2092
|
+
this.audit(peer.ip, "ERROR", err.message);
|
|
2093
|
+
this.emitPeerEvent("error", err.message);
|
|
1989
2094
|
}
|
|
1990
|
-
|
|
1991
|
-
process.stdin.on("keypress", state.keypressHandler);
|
|
1992
|
-
process.once("SIGINT", () => stopLive());
|
|
1993
|
-
}
|
|
1994
|
-
function tabKeyToId(str) {
|
|
1995
|
-
const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process" };
|
|
1996
|
-
return map[str] ?? null;
|
|
1997
|
-
}
|
|
1998
|
-
function cycleTab(direction) {
|
|
1999
|
-
const tabs = state.PANEL_TABS.map((t) => t.id);
|
|
2000
|
-
const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
|
|
2001
|
-
const next = (current + direction + tabs.length) % tabs.length;
|
|
2002
|
-
state.activePanel = tabs[next];
|
|
2003
|
-
setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
|
|
2004
|
-
render();
|
|
2005
|
-
}
|
|
2006
|
-
function handleNormalMouse(seq) {
|
|
2007
|
-
if (seq.length < 6) return;
|
|
2008
|
-
const cb = seq.charCodeAt(3) - 32;
|
|
2009
|
-
const cx = seq.charCodeAt(4) - 32;
|
|
2010
|
-
const cy = seq.charCodeAt(5) - 32;
|
|
2011
|
-
if (cb === 0 || cb === 1) {
|
|
2012
|
-
handleMouseClick(cy, cx);
|
|
2095
|
+
this.cleanupPeer(peer);
|
|
2013
2096
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const cy = parseInt(match[3]);
|
|
2021
|
-
if (button === 0 || button === 1) {
|
|
2022
|
-
handleMouseClick(cy, cx);
|
|
2097
|
+
disconnectPeer(peer, reason) {
|
|
2098
|
+
try {
|
|
2099
|
+
this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
|
|
2100
|
+
} catch {
|
|
2101
|
+
}
|
|
2102
|
+
peer.socket.destroy();
|
|
2023
2103
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2104
|
+
cleanupPeer(peer) {
|
|
2105
|
+
if (peer.intervalHandle) {
|
|
2106
|
+
clearInterval(peer.intervalHandle);
|
|
2107
|
+
peer.intervalHandle = null;
|
|
2108
|
+
}
|
|
2109
|
+
if (peer.pingHandle) {
|
|
2110
|
+
clearInterval(peer.pingHandle);
|
|
2111
|
+
peer.pingHandle = null;
|
|
2112
|
+
}
|
|
2113
|
+
this.peers.delete(peer.socket);
|
|
2033
2114
|
}
|
|
2115
|
+
};
|
|
2116
|
+
async function startP2PServer(options) {
|
|
2117
|
+
const server = new P2PServer(options);
|
|
2118
|
+
await server.start();
|
|
2119
|
+
return server;
|
|
2034
2120
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2121
|
+
|
|
2122
|
+
// src/p2p/client.ts
|
|
2123
|
+
import net2 from "net";
|
|
2124
|
+
import tls2 from "tls";
|
|
2125
|
+
import fs3 from "fs";
|
|
2126
|
+
import crypto3 from "crypto";
|
|
2127
|
+
import chalk7 from "chalk";
|
|
2128
|
+
var P2PClient = class {
|
|
2129
|
+
socket = null;
|
|
2130
|
+
options;
|
|
2131
|
+
running = false;
|
|
2132
|
+
readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
2133
|
+
onDataCallback = null;
|
|
2134
|
+
onStatusCallback = null;
|
|
2135
|
+
reconnectTimer = null;
|
|
2136
|
+
pingHandle = null;
|
|
2137
|
+
pendingNonce = null;
|
|
2138
|
+
auditStream = null;
|
|
2139
|
+
hmacKey;
|
|
2140
|
+
constructor(options) {
|
|
2141
|
+
this.options = options;
|
|
2142
|
+
this.hmacKey = deriveHMACKey2(options.password);
|
|
2043
2143
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
state.logStream = null;
|
|
2144
|
+
setOnData(cb) {
|
|
2145
|
+
this.onDataCallback = cb;
|
|
2047
2146
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
state.keypressHandler = null;
|
|
2147
|
+
setOnStatus(cb) {
|
|
2148
|
+
this.onStatusCallback = cb;
|
|
2051
2149
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2150
|
+
async connect() {
|
|
2151
|
+
if (this.options.auditLog) {
|
|
2152
|
+
try {
|
|
2153
|
+
fs3.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2154
|
+
const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
|
|
2155
|
+
this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
|
|
2156
|
+
} catch {
|
|
2157
|
+
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return new Promise((resolve, reject) => {
|
|
2161
|
+
const useTLS = !!this.options.tls;
|
|
2162
|
+
if (useTLS) {
|
|
2163
|
+
const tlsOptions = {
|
|
2164
|
+
host: this.options.host,
|
|
2165
|
+
port: this.options.port,
|
|
2166
|
+
rejectUnauthorized: !this.options.tlsInsecure
|
|
2167
|
+
};
|
|
2168
|
+
if (this.options.tlsCA) {
|
|
2169
|
+
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2170
|
+
}
|
|
2171
|
+
this.socket = tls2.connect(tlsOptions, () => {
|
|
2172
|
+
this.status("Connecting...");
|
|
2173
|
+
});
|
|
2174
|
+
} else {
|
|
2175
|
+
this.socket = new net2.Socket();
|
|
2176
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2177
|
+
this.socket.on("close", () => this.onClose());
|
|
2178
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2179
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2180
|
+
this.socket.setTimeout(1e4);
|
|
2181
|
+
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2182
|
+
this.status("Connecting...");
|
|
2183
|
+
});
|
|
2184
|
+
this.socket.once("error", (err) => {
|
|
2185
|
+
reject(err);
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
this.socket.on("data", (data) => this.onData(data));
|
|
2189
|
+
this.socket.on("close", () => this.onClose());
|
|
2190
|
+
this.socket.on("error", (err) => this.onError(err));
|
|
2191
|
+
this.socket.on("timeout", () => this.onTimeout());
|
|
2192
|
+
});
|
|
2059
2193
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
"#3f1004",
|
|
2075
|
-
"#591404",
|
|
2076
|
-
"#791804",
|
|
2077
|
-
"#9e1c04",
|
|
2078
|
-
"#c92004",
|
|
2079
|
-
"#d63004",
|
|
2080
|
-
"#e34004",
|
|
2081
|
-
"#f05004",
|
|
2082
|
-
"#fd6004",
|
|
2083
|
-
"#ff7004",
|
|
2084
|
-
"#ff8004",
|
|
2085
|
-
"#ff9004",
|
|
2086
|
-
"#ffa004",
|
|
2087
|
-
"#ffb004",
|
|
2088
|
-
"#ffc004",
|
|
2089
|
-
"#ffd004",
|
|
2090
|
-
"#ffe004",
|
|
2091
|
-
"#fff004",
|
|
2092
|
-
"#fff820",
|
|
2093
|
-
"#ffff40",
|
|
2094
|
-
"#ffff60",
|
|
2095
|
-
"#ffff80",
|
|
2096
|
-
"#ffffa0",
|
|
2097
|
-
"#ffffc0",
|
|
2098
|
-
"#ffffe0",
|
|
2099
|
-
"#ffffff",
|
|
2100
|
-
"#ffffff",
|
|
2101
|
-
"#ffffff",
|
|
2102
|
-
"#ffffff",
|
|
2103
|
-
"#ffffff",
|
|
2104
|
-
"#ffffff",
|
|
2105
|
-
"#ffffff",
|
|
2106
|
-
"#ffffff"
|
|
2107
|
-
];
|
|
2108
|
-
function getRgb(hex) {
|
|
2109
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
2110
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
2111
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
2112
|
-
return { r, g, b };
|
|
2113
|
-
}
|
|
2114
|
-
var rgbPalette = palette.map(getRgb);
|
|
2115
|
-
function getChar(val) {
|
|
2116
|
-
const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
|
|
2117
|
-
return CHAR_RAMP[idx];
|
|
2118
|
-
}
|
|
2119
|
-
async function showSplash() {
|
|
2120
|
-
const cols = 60;
|
|
2121
|
-
const rows = 15;
|
|
2122
|
-
let fire = new Uint8Array(cols * rows);
|
|
2123
|
-
process.stdout.write("\x1B[2J");
|
|
2124
|
-
const termRows = process.stdout.rows || 24;
|
|
2125
|
-
const termCols = process.stdout.columns || 80;
|
|
2126
|
-
const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
|
|
2127
|
-
const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
|
|
2128
|
-
return new Promise((resolve) => {
|
|
2129
|
-
const interval = setInterval(() => {
|
|
2130
|
-
for (let x = 0; x < cols; x++) {
|
|
2131
|
-
fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
|
|
2194
|
+
disconnect() {
|
|
2195
|
+
this.running = false;
|
|
2196
|
+
if (this.reconnectTimer) {
|
|
2197
|
+
clearTimeout(this.reconnectTimer);
|
|
2198
|
+
this.reconnectTimer = null;
|
|
2199
|
+
}
|
|
2200
|
+
if (this.pingHandle) {
|
|
2201
|
+
clearInterval(this.pingHandle);
|
|
2202
|
+
this.pingHandle = null;
|
|
2203
|
+
}
|
|
2204
|
+
if (this.socket) {
|
|
2205
|
+
try {
|
|
2206
|
+
this.sendSigned(encodeDisconnect("Client closing"));
|
|
2207
|
+
} catch {
|
|
2132
2208
|
}
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2209
|
+
this.socket.destroy();
|
|
2210
|
+
this.socket = null;
|
|
2211
|
+
}
|
|
2212
|
+
if (this.auditStream) {
|
|
2213
|
+
this.auditStream.end();
|
|
2214
|
+
this.auditStream = null;
|
|
2215
|
+
}
|
|
2216
|
+
this.status("Disconnected");
|
|
2217
|
+
}
|
|
2218
|
+
onData(data) {
|
|
2219
|
+
this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
|
|
2220
|
+
while (this.readState.buffer.length > 0) {
|
|
2221
|
+
if (this.readState.expectingHeader) {
|
|
2222
|
+
if (this.readState.buffer.length >= 4) {
|
|
2223
|
+
this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
|
|
2224
|
+
this.readState.buffer = this.readState.buffer.subarray(4);
|
|
2225
|
+
this.readState.expectingHeader = false;
|
|
2226
|
+
} else {
|
|
2227
|
+
break;
|
|
2147
2228
|
}
|
|
2148
2229
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2230
|
+
if (!this.readState.expectingHeader) {
|
|
2231
|
+
if (this.readState.buffer.length >= this.readState.messageLength) {
|
|
2232
|
+
const body = this.readState.buffer.subarray(0, this.readState.messageLength);
|
|
2233
|
+
this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
|
|
2234
|
+
this.readState.expectingHeader = true;
|
|
2235
|
+
this.handleMessage(body);
|
|
2236
|
+
} else {
|
|
2237
|
+
break;
|
|
2157
2238
|
}
|
|
2158
|
-
output += "\n";
|
|
2159
2239
|
}
|
|
2160
|
-
|
|
2161
|
-
output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk6.dim("[Press Enter to continue]");
|
|
2162
|
-
process.stdout.write(`\x1B[H${output}`);
|
|
2163
|
-
}, 65);
|
|
2164
|
-
const onEnter = () => {
|
|
2165
|
-
process.stdin.removeListener("data", onEnter);
|
|
2166
|
-
clearInterval(interval);
|
|
2167
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
2168
|
-
resolve();
|
|
2169
|
-
};
|
|
2170
|
-
process.stdin.setRawMode(true);
|
|
2171
|
-
process.stdin.resume();
|
|
2172
|
-
process.stdin.on("data", onEnter);
|
|
2173
|
-
setTimeout(onEnter, 5e3);
|
|
2174
|
-
});
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
// src/p2p/server.ts
|
|
2178
|
-
import net from "net";
|
|
2179
|
-
import tls from "tls";
|
|
2180
|
-
import fs2 from "fs";
|
|
2181
|
-
import crypto2 from "crypto";
|
|
2182
|
-
import chalk7 from "chalk";
|
|
2183
|
-
|
|
2184
|
-
// src/p2p/protocol.ts
|
|
2185
|
-
import crypto from "crypto";
|
|
2186
|
-
var HEADER_SIZE = 4;
|
|
2187
|
-
var MAX_MESSAGE_SIZE = 16 * 1024 * 1024;
|
|
2188
|
-
var HMAC_ALGO = "sha256";
|
|
2189
|
-
function buildMessage(kind, payload, hmac) {
|
|
2190
|
-
const envelope = { kind, payload };
|
|
2191
|
-
if (hmac) envelope.hmac = hmac;
|
|
2192
|
-
const body = Buffer.from(JSON.stringify(envelope), "utf-8");
|
|
2193
|
-
const header2 = Buffer.alloc(HEADER_SIZE);
|
|
2194
|
-
header2.writeUInt32BE(body.length, 0);
|
|
2195
|
-
return Buffer.concat([header2, body]);
|
|
2196
|
-
}
|
|
2197
|
-
function generateNonce() {
|
|
2198
|
-
return crypto.randomBytes(32).toString("hex");
|
|
2199
|
-
}
|
|
2200
|
-
function hashPassword(password, nonce) {
|
|
2201
|
-
return crypto.createHash("sha256").update(`${password}:${nonce}`).digest("hex");
|
|
2202
|
-
}
|
|
2203
|
-
function computeHMAC(body, key) {
|
|
2204
|
-
if (!key) return void 0;
|
|
2205
|
-
return crypto.createHmac(HMAC_ALGO, key).update(body).digest("hex");
|
|
2206
|
-
}
|
|
2207
|
-
function verifyHMAC(body, expectedHmac, key) {
|
|
2208
|
-
if (!key) return expectedHmac === void 0;
|
|
2209
|
-
if (!expectedHmac) return false;
|
|
2210
|
-
try {
|
|
2211
|
-
const parsed = JSON.parse(body);
|
|
2212
|
-
const payloadWithoutHmac = { ...parsed, hmac: void 0 };
|
|
2213
|
-
const canonical = JSON.stringify(payloadWithoutHmac);
|
|
2214
|
-
const computed = crypto.createHmac(HMAC_ALGO, key).update(canonical).digest("hex");
|
|
2215
|
-
return crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(expectedHmac));
|
|
2216
|
-
} catch {
|
|
2217
|
-
return false;
|
|
2240
|
+
}
|
|
2218
2241
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
return msg;
|
|
2227
|
-
}
|
|
2228
|
-
function signMessage(kind, payload, key) {
|
|
2229
|
-
const envelope = { kind, payload };
|
|
2230
|
-
const bodyJson = JSON.stringify(envelope);
|
|
2231
|
-
const hmac = computeHMAC(bodyJson, key);
|
|
2232
|
-
return buildMessage(kind, payload, hmac);
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
// src/p2p/server.ts
|
|
2236
|
-
var HEADER_SIZE2 = 4;
|
|
2237
|
-
var DEFAULT_RATE_LIMIT_MAX = 5;
|
|
2238
|
-
var DEFAULT_RATE_LIMIT_WINDOW = 6e4;
|
|
2239
|
-
var HMAC_KEY_LENGTH = 32;
|
|
2240
|
-
function readMessageFromPeer(peer) {
|
|
2241
|
-
while (peer.readBuffer.length > 0) {
|
|
2242
|
-
if (peer.readExpectingHeader) {
|
|
2243
|
-
if (peer.readBuffer.length >= HEADER_SIZE2) {
|
|
2244
|
-
peer.readMessageLength = peer.readBuffer.readUInt32BE(0);
|
|
2245
|
-
peer.readBuffer = peer.readBuffer.subarray(HEADER_SIZE2);
|
|
2246
|
-
peer.readExpectingHeader = false;
|
|
2247
|
-
} else {
|
|
2248
|
-
break;
|
|
2242
|
+
handleMessage(body) {
|
|
2243
|
+
try {
|
|
2244
|
+
const raw = body.toString("utf-8");
|
|
2245
|
+
const msg = decodeMessage(body);
|
|
2246
|
+
if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
|
|
2247
|
+
this.status("Message integrity check failed");
|
|
2248
|
+
return;
|
|
2249
2249
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2250
|
+
switch (msg.kind) {
|
|
2251
|
+
case "challenge":
|
|
2252
|
+
const challengePayload = msg.payload;
|
|
2253
|
+
if (challengePayload.nonce) {
|
|
2254
|
+
this.pendingNonce = challengePayload.nonce;
|
|
2255
|
+
const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
|
|
2256
|
+
this.sendSigned(signMessage("auth", { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
|
|
2257
|
+
this.status("Authenticating...");
|
|
2258
|
+
}
|
|
2259
|
+
break;
|
|
2260
|
+
case "auth-ok":
|
|
2261
|
+
this.status("Authenticated \u2014 receiving live data");
|
|
2262
|
+
this.startPing();
|
|
2263
|
+
break;
|
|
2264
|
+
case "auth-fail":
|
|
2265
|
+
const failPayload = msg.payload;
|
|
2266
|
+
this.status(`Authentication failed: ${failPayload.reason || "unknown"}`);
|
|
2267
|
+
this.disconnect();
|
|
2268
|
+
break;
|
|
2269
|
+
case "data":
|
|
2270
|
+
if (this.onDataCallback) {
|
|
2271
|
+
const dataPayload = msg.payload;
|
|
2272
|
+
if (dataPayload.stats) {
|
|
2273
|
+
this.onDataCallback(dataPayload.stats);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
break;
|
|
2277
|
+
case "ping":
|
|
2278
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2279
|
+
const ts = msg.payload.ts;
|
|
2280
|
+
this.sendSigned(signMessage("pong", { ts }, this.hmacKey));
|
|
2281
|
+
}
|
|
2282
|
+
break;
|
|
2283
|
+
case "pong":
|
|
2284
|
+
break;
|
|
2285
|
+
case "disconnect":
|
|
2286
|
+
const disconnectPayload = msg.payload;
|
|
2287
|
+
this.status(`Server disconnected: ${disconnectPayload.reason || "unknown"}`);
|
|
2288
|
+
this.disconnect();
|
|
2289
|
+
break;
|
|
2290
|
+
default:
|
|
2291
|
+
break;
|
|
2259
2292
|
}
|
|
2293
|
+
} catch {
|
|
2260
2294
|
}
|
|
2261
2295
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
for (const denied of deniedIPs) {
|
|
2266
|
-
if (ip === denied || ip.startsWith(denied)) {
|
|
2267
|
-
return false;
|
|
2296
|
+
sendSigned(message) {
|
|
2297
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2298
|
+
this.socket.write(message);
|
|
2268
2299
|
}
|
|
2269
2300
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2301
|
+
startPing() {
|
|
2302
|
+
this.pingHandle = setInterval(() => {
|
|
2303
|
+
if (this.socket && !this.socket.destroyed) {
|
|
2304
|
+
this.sendSigned(signMessage("ping", { ts: Date.now() }, this.hmacKey));
|
|
2305
|
+
}
|
|
2306
|
+
}, 15e3);
|
|
2272
2307
|
}
|
|
2273
|
-
|
|
2274
|
-
if (
|
|
2275
|
-
|
|
2308
|
+
onClose() {
|
|
2309
|
+
if (this.running) {
|
|
2310
|
+
this.status("Connection closed \u2014 reconnecting in 3s");
|
|
2311
|
+
this.reconnectTimer = setTimeout(() => {
|
|
2312
|
+
if (this.running) this.connect().catch(() => {
|
|
2313
|
+
});
|
|
2314
|
+
}, 3e3);
|
|
2276
2315
|
}
|
|
2277
2316
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
rateLimitMap = /* @__PURE__ */ new Map();
|
|
2289
|
-
auditStream = null;
|
|
2290
|
-
hmacKey;
|
|
2291
|
-
constructor(options) {
|
|
2292
|
-
this.options = options;
|
|
2293
|
-
this.hmacKey = deriveHMACKey(options.password);
|
|
2294
|
-
}
|
|
2295
|
-
get info() {
|
|
2296
|
-
return {
|
|
2297
|
-
host: this.options.host,
|
|
2298
|
-
port: this.options.port,
|
|
2299
|
-
peersConnected: this.peers.size
|
|
2300
|
-
};
|
|
2301
|
-
}
|
|
2302
|
-
async start() {
|
|
2303
|
-
if (this.options.auditLog) {
|
|
2304
|
-
try {
|
|
2305
|
-
fs2.mkdirSync(this.options.auditLog, { recursive: true });
|
|
2306
|
-
const logFile = `${this.options.auditLog}/p2p-audit.log`;
|
|
2307
|
-
this.auditStream = fs2.createWriteStream(logFile, { flags: "a" });
|
|
2308
|
-
} catch {
|
|
2309
|
-
console.log(chalk7.yellow("Warning: could not open audit log"));
|
|
2310
|
-
}
|
|
2317
|
+
onError(err) {
|
|
2318
|
+
const code = err.code;
|
|
2319
|
+
if (code === "ECONNREFUSED") {
|
|
2320
|
+
this.status("Connection refused \u2014 is the server running?");
|
|
2321
|
+
} else if (code === "ETIMEDOUT") {
|
|
2322
|
+
this.status("Connection timed out");
|
|
2323
|
+
} else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
2324
|
+
this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
|
|
2325
|
+
} else {
|
|
2326
|
+
this.status(`Connection error: ${err.message}`);
|
|
2311
2327
|
}
|
|
2312
|
-
return new Promise((resolve, reject) => {
|
|
2313
|
-
const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
|
|
2314
|
-
if (useTLS) {
|
|
2315
|
-
const tlsOptions = {
|
|
2316
|
-
key: fs2.readFileSync(this.options.tlsKey),
|
|
2317
|
-
cert: fs2.readFileSync(this.options.tlsCert)
|
|
2318
|
-
};
|
|
2319
|
-
if (this.options.tlsCA) {
|
|
2320
|
-
tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
|
|
2321
|
-
}
|
|
2322
|
-
this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
|
|
2323
|
-
} else {
|
|
2324
|
-
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
2325
|
-
}
|
|
2326
|
-
this.server.on("error", (err) => reject(err));
|
|
2327
|
-
this.server.listen(this.options.port, this.options.host, () => {
|
|
2328
|
-
this.running = true;
|
|
2329
|
-
const addr = this.server?.address();
|
|
2330
|
-
const bound = addr ? typeof addr === "object" ? `${addr.address}:${addr.port}` : String(addr) : "unknown";
|
|
2331
|
-
const protocol = useTLS ? "TLS" : "TCP";
|
|
2332
|
-
console.log(chalk7.green(`P2P server listening on ${bound} (${protocol})`));
|
|
2333
|
-
console.log(chalk7.dim(` Password: ${this.options.password}`));
|
|
2334
|
-
console.log(chalk7.dim(` Interval: ${this.options.intervalMs}ms`));
|
|
2335
|
-
const allowedIPs = this.options.allowedIPs ?? [];
|
|
2336
|
-
const deniedIPs = this.options.deniedIPs ?? [];
|
|
2337
|
-
if (allowedIPs.length > 0) console.log(chalk7.dim(` Allowed IPs: ${allowedIPs.join(", ")}`));
|
|
2338
|
-
if (deniedIPs.length > 0) console.log(chalk7.dim(` Denied IPs: ${deniedIPs.join(", ")}`));
|
|
2339
|
-
resolve();
|
|
2340
|
-
});
|
|
2341
|
-
});
|
|
2342
2328
|
}
|
|
2343
|
-
|
|
2344
|
-
this.
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
this.
|
|
2349
|
-
|
|
2350
|
-
if (this.server) {
|
|
2351
|
-
this.server.close();
|
|
2352
|
-
this.server = null;
|
|
2353
|
-
}
|
|
2354
|
-
if (this.auditStream) {
|
|
2355
|
-
this.auditStream.end();
|
|
2356
|
-
this.auditStream = null;
|
|
2329
|
+
onTimeout() {
|
|
2330
|
+
this.status("Connection timed out");
|
|
2331
|
+
this.disconnect();
|
|
2332
|
+
}
|
|
2333
|
+
status(msg) {
|
|
2334
|
+
if (this.onStatusCallback) {
|
|
2335
|
+
this.onStatusCallback(msg);
|
|
2357
2336
|
}
|
|
2358
2337
|
}
|
|
2359
|
-
audit(
|
|
2338
|
+
audit(event, detail) {
|
|
2360
2339
|
if (!this.auditStream) return;
|
|
2361
2340
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2341
|
+
const ip = this.options.host;
|
|
2362
2342
|
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2363
2343
|
`);
|
|
2364
2344
|
}
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
const entry = this.rateLimitMap.get(ip);
|
|
2378
|
-
if (!entry) {
|
|
2379
|
-
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
2380
|
-
return true;
|
|
2381
|
-
}
|
|
2382
|
-
if (now - entry.firstAttempt > windowMs) {
|
|
2383
|
-
this.rateLimitMap.set(ip, { attempts: 1, firstAttempt: now });
|
|
2384
|
-
return true;
|
|
2385
|
-
}
|
|
2386
|
-
entry.attempts++;
|
|
2387
|
-
if (entry.attempts > maxAttempts) {
|
|
2388
|
-
return false;
|
|
2389
|
-
}
|
|
2390
|
-
return true;
|
|
2391
|
-
}
|
|
2392
|
-
cleanupRateLimit(ip) {
|
|
2393
|
-
this.rateLimitMap.delete(ip);
|
|
2345
|
+
};
|
|
2346
|
+
function deriveHMACKey2(password) {
|
|
2347
|
+
return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/live/input.ts
|
|
2351
|
+
function handleInputModeKey(str, key) {
|
|
2352
|
+
if (key.name === "escape") {
|
|
2353
|
+
state.inputMode = null;
|
|
2354
|
+
state.inputBuffer = "";
|
|
2355
|
+
render();
|
|
2356
|
+
return;
|
|
2394
2357
|
}
|
|
2395
|
-
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
if (
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
authenticated: false,
|
|
2414
|
-
intervalHandle: null,
|
|
2415
|
-
pingHandle: null,
|
|
2416
|
-
readBuffer: Buffer.alloc(0),
|
|
2417
|
-
readExpectingHeader: true,
|
|
2418
|
-
readMessageLength: 0,
|
|
2419
|
-
ip,
|
|
2420
|
-
pendingNonce: null
|
|
2421
|
-
};
|
|
2422
|
-
this.peers.set(socket, peer);
|
|
2423
|
-
const remoteAddr = socket.remoteAddress ? `${socket.remoteAddress}:${socket.remotePort}` : "unknown";
|
|
2424
|
-
console.log(chalk7.cyan(`Peer connected: ${remoteAddr}`));
|
|
2425
|
-
this.audit(ip, "CONNECT", remoteAddr);
|
|
2426
|
-
socket.on("data", (data) => this.onData(peer, data));
|
|
2427
|
-
socket.on("close", () => this.onPeerClose(peer));
|
|
2428
|
-
socket.on("error", (err) => this.onPeerError(peer, err));
|
|
2429
|
-
socket.setTimeout(3e4);
|
|
2430
|
-
socket.on("timeout", () => {
|
|
2431
|
-
if (!peer.authenticated) {
|
|
2432
|
-
this.disconnectPeer(peer, "Authentication timeout");
|
|
2358
|
+
if (state.inputMode === "customizer") {
|
|
2359
|
+
const themesList = Object.keys(THEMES);
|
|
2360
|
+
if (key.name === "up" || str === "k") {
|
|
2361
|
+
state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
|
|
2362
|
+
} else if (key.name === "down" || str === "j") {
|
|
2363
|
+
state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
|
|
2364
|
+
} else if (key.name === "return" || str === " ") {
|
|
2365
|
+
const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
|
|
2366
|
+
if (selected === "Theme") {
|
|
2367
|
+
const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
|
|
2368
|
+
state.currentTheme = themesList[nextIdx];
|
|
2369
|
+
} else if (selected === "Toggle Tree View") {
|
|
2370
|
+
state.treeView = !state.treeView;
|
|
2371
|
+
} else if (selected === "Graph Mode") {
|
|
2372
|
+
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
2373
|
+
} else {
|
|
2374
|
+
const itemKey = selected.replace("Toggle ", "").toLowerCase();
|
|
2375
|
+
state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
|
|
2433
2376
|
}
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
peer.pendingNonce = nonce;
|
|
2377
|
+
}
|
|
2378
|
+
render();
|
|
2379
|
+
return;
|
|
2438
2380
|
}
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2381
|
+
if (state.inputMode === "signal") {
|
|
2382
|
+
if (key.name === "up" || str === "k") {
|
|
2383
|
+
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
2384
|
+
state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
|
|
2385
|
+
} else if (key.name === "down" || str === "j") {
|
|
2386
|
+
const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
|
|
2387
|
+
state.inputBuffer = state.SIGNAL_OPTIONS[(idx + 1) % state.SIGNAL_OPTIONS.length];
|
|
2388
|
+
} else if (key.name === "return" || str === " ") {
|
|
2389
|
+
killProcess(state.inputBuffer.trim(), state.inputBuffer.trim());
|
|
2390
|
+
state.inputMode = null;
|
|
2391
|
+
state.inputBuffer = "";
|
|
2392
|
+
render();
|
|
2446
2393
|
return;
|
|
2447
2394
|
}
|
|
2395
|
+
render();
|
|
2396
|
+
return;
|
|
2448
2397
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
this.disconnectPeer(peer, "Invalid auth payload");
|
|
2462
|
-
return;
|
|
2463
|
-
}
|
|
2464
|
-
if (peer.pendingNonce && authPayload.nonce !== peer.pendingNonce) {
|
|
2465
|
-
this.audit(peer.ip, "AUTH_FAIL", "Nonce mismatch");
|
|
2466
|
-
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid nonce" }, this.hmacKey));
|
|
2467
|
-
this.disconnectPeer(peer, "Invalid nonce");
|
|
2468
|
-
return;
|
|
2469
|
-
}
|
|
2470
|
-
const expectedHash = hashPassword(this.options.password, authPayload.nonce);
|
|
2471
|
-
if (authPayload.passwordHash !== expectedHash) {
|
|
2472
|
-
this.audit(peer.ip, "AUTH_FAIL", "Invalid password hash");
|
|
2473
|
-
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid password" }, this.hmacKey));
|
|
2474
|
-
this.disconnectPeer(peer, "Authentication failed");
|
|
2475
|
-
return;
|
|
2398
|
+
if (state.inputMode === "p2p") {
|
|
2399
|
+
if (key.name === "escape") {
|
|
2400
|
+
state.inputMode = null;
|
|
2401
|
+
state.inputBuffer = "";
|
|
2402
|
+
render();
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (key.name === "return") {
|
|
2406
|
+
const password = state.inputBuffer.trim();
|
|
2407
|
+
if (password) {
|
|
2408
|
+
state.p2pPassword = password;
|
|
2409
|
+
startP2PServerFromTUI();
|
|
2476
2410
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
console.log(chalk7.green(`Peer authenticated: ${peer.socket.remoteAddress}`));
|
|
2482
|
-
this.audit(peer.ip, "AUTH_OK", "Challenge-response successful");
|
|
2483
|
-
this.startDataStream(peer);
|
|
2484
|
-
this.startPing(peer);
|
|
2485
|
-
} catch {
|
|
2486
|
-
peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
|
|
2487
|
-
this.disconnectPeer(peer, "Invalid message format");
|
|
2411
|
+
state.inputMode = null;
|
|
2412
|
+
state.inputBuffer = "";
|
|
2413
|
+
render();
|
|
2414
|
+
return;
|
|
2488
2415
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2416
|
+
if (key.name === "backspace") {
|
|
2417
|
+
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
2418
|
+
render();
|
|
2419
|
+
return;
|
|
2493
2420
|
}
|
|
2421
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
2422
|
+
state.inputBuffer += str;
|
|
2423
|
+
render();
|
|
2424
|
+
}
|
|
2425
|
+
return;
|
|
2494
2426
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2427
|
+
if (key.name === "return") {
|
|
2428
|
+
if (state.inputMode === "filter") {
|
|
2429
|
+
state.processFilter = state.inputBuffer.trim();
|
|
2430
|
+
setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
|
|
2431
|
+
} else if (state.inputMode === "kill") {
|
|
2432
|
+
killProcess(state.inputBuffer.trim());
|
|
2433
|
+
}
|
|
2434
|
+
state.inputMode = null;
|
|
2435
|
+
state.inputBuffer = "";
|
|
2436
|
+
render();
|
|
2437
|
+
return;
|
|
2506
2438
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
}, 5e3);
|
|
2439
|
+
if (key.name === "backspace") {
|
|
2440
|
+
state.inputBuffer = state.inputBuffer.slice(0, -1);
|
|
2441
|
+
render();
|
|
2442
|
+
return;
|
|
2512
2443
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
|
|
2444
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
2445
|
+
state.inputBuffer += str;
|
|
2446
|
+
render();
|
|
2517
2447
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
console.log(chalk7.red(`Peer error: ${err.message}`));
|
|
2525
|
-
this.audit(peer.ip, "ERROR", err.message);
|
|
2526
|
-
}
|
|
2527
|
-
this.cleanupPeer(peer);
|
|
2448
|
+
}
|
|
2449
|
+
function killProcess(pidStr, signal = "SIGTERM") {
|
|
2450
|
+
const pid = parseInt(pidStr, 10);
|
|
2451
|
+
if (!pid || pid <= 0) {
|
|
2452
|
+
setStatus(`Invalid PID: ${pidStr}`);
|
|
2453
|
+
return;
|
|
2528
2454
|
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
}
|
|
2534
|
-
peer.socket.destroy();
|
|
2455
|
+
try {
|
|
2456
|
+
process.kill(pid, signal);
|
|
2457
|
+
setStatus(`Sent ${signal} to PID ${pid}`);
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
setStatus(`Failed to send ${signal} to ${pid}: ${err.message}`);
|
|
2535
2460
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2461
|
+
}
|
|
2462
|
+
async function startP2PServerFromTUI() {
|
|
2463
|
+
if (state.p2pServerRunning) return;
|
|
2464
|
+
try {
|
|
2465
|
+
const server = await startP2PServer({
|
|
2466
|
+
host: "0.0.0.0",
|
|
2467
|
+
port: state.p2pPort,
|
|
2468
|
+
password: state.p2pPassword,
|
|
2469
|
+
intervalMs: state.interval * 1e3,
|
|
2470
|
+
detailed: state.detailed,
|
|
2471
|
+
onLog: (msg) => setStatus(msg, 5e3),
|
|
2472
|
+
onPeerEvent: (evt) => {
|
|
2473
|
+
state.p2pEvents.push(evt);
|
|
2474
|
+
if (state.p2pEvents.length > 200) state.p2pEvents.shift();
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
state.p2pServer = server;
|
|
2478
|
+
state.p2pServerRunning = true;
|
|
2479
|
+
state.p2pBind = server.boundAddress || "0.0.0.0";
|
|
2480
|
+
state.p2pEvents = [];
|
|
2481
|
+
setStatus(`P2P server started on ${state.p2pBind}:${state.p2pPort}`);
|
|
2482
|
+
render();
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
setStatus(`P2P server failed: ${err.message}`);
|
|
2485
|
+
state.p2pServerRunning = false;
|
|
2486
|
+
state.p2pServer = null;
|
|
2487
|
+
state.p2pBind = "";
|
|
2488
|
+
render();
|
|
2546
2489
|
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
import crypto3 from "crypto";
|
|
2554
|
-
import chalk8 from "chalk";
|
|
2555
|
-
var P2PClient = class {
|
|
2556
|
-
socket = null;
|
|
2557
|
-
options;
|
|
2558
|
-
running = false;
|
|
2559
|
-
readState = { buffer: Buffer.alloc(0), expectingHeader: true, messageLength: 0 };
|
|
2560
|
-
onDataCallback = null;
|
|
2561
|
-
onStatusCallback = null;
|
|
2562
|
-
reconnectTimer = null;
|
|
2563
|
-
pingHandle = null;
|
|
2564
|
-
pendingNonce = null;
|
|
2565
|
-
auditStream = null;
|
|
2566
|
-
hmacKey;
|
|
2567
|
-
constructor(options) {
|
|
2568
|
-
this.options = options;
|
|
2569
|
-
this.hmacKey = deriveHMACKey2(options.password);
|
|
2490
|
+
}
|
|
2491
|
+
function syncP2PEvents() {
|
|
2492
|
+
if (!state.p2pServer) return;
|
|
2493
|
+
const history = state.p2pServer.peerEventHistory;
|
|
2494
|
+
if (history && history !== state.p2pEvents) {
|
|
2495
|
+
state.p2pEvents = history;
|
|
2570
2496
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2497
|
+
}
|
|
2498
|
+
function stopP2PServer() {
|
|
2499
|
+
if (!state.p2pServer) return;
|
|
2500
|
+
try {
|
|
2501
|
+
state.p2pServer.stop();
|
|
2502
|
+
} catch {
|
|
2573
2503
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2504
|
+
state.p2pServer = null;
|
|
2505
|
+
state.p2pServerRunning = false;
|
|
2506
|
+
setStatus("P2P server stopped");
|
|
2507
|
+
}
|
|
2508
|
+
function onResize() {
|
|
2509
|
+
state.termWidth = process.stdout.columns || 80;
|
|
2510
|
+
state.termHeight = process.stdout.rows || 24;
|
|
2511
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
2512
|
+
render();
|
|
2513
|
+
}
|
|
2514
|
+
function restartTicker() {
|
|
2515
|
+
if (state.intervalHandle) clearInterval(state.intervalHandle);
|
|
2516
|
+
state.intervalHandle = setInterval(tick, state.interval * 1e3);
|
|
2517
|
+
}
|
|
2518
|
+
async function doWarmup() {
|
|
2519
|
+
try {
|
|
2520
|
+
const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
|
|
2521
|
+
state.lastData = data;
|
|
2522
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
2523
|
+
state.history.push({
|
|
2524
|
+
cpuUsage: data.cpu.usage,
|
|
2525
|
+
memUsage: data.memory.usagePercent,
|
|
2526
|
+
temp,
|
|
2527
|
+
rxBytes: data.network.rxBytes,
|
|
2528
|
+
txBytes: data.network.txBytes,
|
|
2529
|
+
gpuUtil: data.gpu?.utilization,
|
|
2530
|
+
powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
|
|
2531
|
+
rxPackets: data.network.rxPackets,
|
|
2532
|
+
txPackets: data.network.txPackets,
|
|
2533
|
+
connections: data.network.connections ?? 0
|
|
2534
|
+
});
|
|
2535
|
+
writeLogRow(data);
|
|
2536
|
+
checkAlerts(data);
|
|
2537
|
+
syncP2PEvents();
|
|
2538
|
+
} catch {
|
|
2576
2539
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
if (this.options.tlsCA) {
|
|
2596
|
-
tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
|
|
2597
|
-
}
|
|
2598
|
-
this.socket = tls2.connect(tlsOptions, () => {
|
|
2599
|
-
this.status("Connecting...");
|
|
2600
|
-
});
|
|
2601
|
-
} else {
|
|
2602
|
-
this.socket = new net2.Socket();
|
|
2603
|
-
this.socket.on("data", (data) => this.onData(data));
|
|
2604
|
-
this.socket.on("close", () => this.onClose());
|
|
2605
|
-
this.socket.on("error", (err) => this.onError(err));
|
|
2606
|
-
this.socket.on("timeout", () => this.onTimeout());
|
|
2607
|
-
this.socket.setTimeout(1e4);
|
|
2608
|
-
this.socket.connect(this.options.port, this.options.host, () => {
|
|
2609
|
-
this.status("Connecting...");
|
|
2610
|
-
});
|
|
2611
|
-
this.socket.once("error", (err) => {
|
|
2612
|
-
reject(err);
|
|
2613
|
-
});
|
|
2614
|
-
}
|
|
2615
|
-
this.socket.on("data", (data) => this.onData(data));
|
|
2616
|
-
this.socket.on("close", () => this.onClose());
|
|
2617
|
-
this.socket.on("error", (err) => this.onError(err));
|
|
2618
|
-
this.socket.on("timeout", () => this.onTimeout());
|
|
2540
|
+
}
|
|
2541
|
+
async function tick() {
|
|
2542
|
+
if (state.paused) return;
|
|
2543
|
+
try {
|
|
2544
|
+
const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
|
|
2545
|
+
state.lastData = data;
|
|
2546
|
+
const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
|
|
2547
|
+
state.history.push({
|
|
2548
|
+
cpuUsage: data.cpu.usage,
|
|
2549
|
+
memUsage: data.memory.usagePercent,
|
|
2550
|
+
temp,
|
|
2551
|
+
rxBytes: data.network.rxBytes,
|
|
2552
|
+
txBytes: data.network.txBytes,
|
|
2553
|
+
gpuUtil: data.gpu?.utilization,
|
|
2554
|
+
powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
|
|
2555
|
+
rxPackets: data.network.rxPackets,
|
|
2556
|
+
txPackets: data.network.txPackets,
|
|
2557
|
+
connections: data.network.connections ?? 0
|
|
2619
2558
|
});
|
|
2559
|
+
writeLogRow(data);
|
|
2560
|
+
checkAlerts(data);
|
|
2561
|
+
syncP2PEvents();
|
|
2562
|
+
render();
|
|
2563
|
+
} catch {
|
|
2620
2564
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2565
|
+
}
|
|
2566
|
+
async function startLive(opts2, splashPromise) {
|
|
2567
|
+
if (state.running) return;
|
|
2568
|
+
state.running = true;
|
|
2569
|
+
state.paused = false;
|
|
2570
|
+
state.detailed = !!opts2.detailed;
|
|
2571
|
+
state.interval = opts2.interval;
|
|
2572
|
+
if (opts2.exportDir) state.exportDir = opts2.exportDir;
|
|
2573
|
+
state.termWidth = process.stdout.columns || 80;
|
|
2574
|
+
state.termHeight = process.stdout.rows || 24;
|
|
2575
|
+
state.history.reset();
|
|
2576
|
+
state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
|
|
2577
|
+
const warmupPromise = doWarmup();
|
|
2578
|
+
if (splashPromise) {
|
|
2579
|
+
await splashPromise;
|
|
2580
|
+
}
|
|
2581
|
+
process.stdout.write("\x1B[?1049h");
|
|
2582
|
+
process.stdout.write("\x1B[?25l");
|
|
2583
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2584
|
+
process.title = "pyre";
|
|
2585
|
+
if (state.mouseEnabled) {
|
|
2586
|
+
process.stdout.write("\x1B[?1000h");
|
|
2587
|
+
}
|
|
2588
|
+
await warmupPromise;
|
|
2589
|
+
render();
|
|
2590
|
+
restartTicker();
|
|
2591
|
+
if (opts2.autoLog) startLogging();
|
|
2592
|
+
readline.emitKeypressEvents(process.stdin);
|
|
2593
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
2594
|
+
process.stdin.resume();
|
|
2595
|
+
process.stdout.on("resize", onResize);
|
|
2596
|
+
state.keypressHandler = (str, key) => {
|
|
2597
|
+
if (!key) return;
|
|
2598
|
+
if (key.ctrl && key.name === "c") {
|
|
2599
|
+
stopLive();
|
|
2600
|
+
return;
|
|
2626
2601
|
}
|
|
2627
|
-
if (
|
|
2628
|
-
|
|
2629
|
-
|
|
2602
|
+
if (state.inputMode) {
|
|
2603
|
+
handleInputModeKey(str, key);
|
|
2604
|
+
return;
|
|
2630
2605
|
}
|
|
2631
|
-
if (
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
} catch {
|
|
2635
|
-
}
|
|
2636
|
-
this.socket.destroy();
|
|
2637
|
-
this.socket = null;
|
|
2606
|
+
if (key.mouse) {
|
|
2607
|
+
handleMouseClick(key.mouse);
|
|
2608
|
+
return;
|
|
2638
2609
|
}
|
|
2639
|
-
if (
|
|
2640
|
-
|
|
2641
|
-
|
|
2610
|
+
if (key.sequence && key.sequence.startsWith("\x1B[M")) {
|
|
2611
|
+
handleNormalMouse(key.sequence);
|
|
2612
|
+
return;
|
|
2642
2613
|
}
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2614
|
+
if (key.sequence && key.sequence.startsWith("\x1B[<")) {
|
|
2615
|
+
handleSGRMouse(key.sequence);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
switch (key.sequence) {
|
|
2619
|
+
case "+":
|
|
2620
|
+
state.interval += 1;
|
|
2621
|
+
restartTicker();
|
|
2622
|
+
setStatus(`Interval set to ${state.interval}s`);
|
|
2623
|
+
return;
|
|
2624
|
+
case "-":
|
|
2625
|
+
state.interval = Math.max(1, state.interval - 1);
|
|
2626
|
+
restartTicker();
|
|
2627
|
+
setStatus(`Interval set to ${state.interval}s`);
|
|
2628
|
+
return;
|
|
2629
|
+
case "/":
|
|
2630
|
+
state.inputMode = "filter";
|
|
2631
|
+
state.inputBuffer = state.processFilter;
|
|
2632
|
+
render();
|
|
2633
|
+
return;
|
|
2634
|
+
case "0":
|
|
2635
|
+
case "1":
|
|
2636
|
+
case "2":
|
|
2637
|
+
case "3":
|
|
2638
|
+
case "4":
|
|
2639
|
+
case "5":
|
|
2640
|
+
case "6":
|
|
2641
|
+
case "7":
|
|
2642
|
+
case "8":
|
|
2643
|
+
case "9": {
|
|
2644
|
+
const tabMap = {
|
|
2645
|
+
"0": "disk",
|
|
2646
|
+
"1": "cpu",
|
|
2647
|
+
"2": "mem",
|
|
2648
|
+
"3": "gpu",
|
|
2649
|
+
"4": "power",
|
|
2650
|
+
"5": "battery",
|
|
2651
|
+
"6": "thermal",
|
|
2652
|
+
"7": "network",
|
|
2653
|
+
"8": "packets",
|
|
2654
|
+
"9": "tasks"
|
|
2655
|
+
};
|
|
2656
|
+
const panelId = tabMap[key.sequence];
|
|
2657
|
+
if (panelId) {
|
|
2658
|
+
state.activePanel = state.activePanel === panelId ? "grid" : panelId;
|
|
2659
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
|
|
2660
|
+
render();
|
|
2655
2661
|
}
|
|
2662
|
+
return;
|
|
2656
2663
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
this.handleMessage(body);
|
|
2663
|
-
} else {
|
|
2664
|
-
break;
|
|
2665
|
-
}
|
|
2664
|
+
case "P": {
|
|
2665
|
+
state.activePanel = state.activePanel === "process" ? "grid" : "process";
|
|
2666
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
|
|
2667
|
+
render();
|
|
2668
|
+
return;
|
|
2666
2669
|
}
|
|
2667
2670
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2671
|
+
switch (key.name) {
|
|
2672
|
+
case "r":
|
|
2673
|
+
if (state.p2pServerRunning) {
|
|
2674
|
+
stopP2PServer();
|
|
2675
|
+
setStatus("P2P server stopped");
|
|
2676
|
+
render();
|
|
2677
|
+
} else {
|
|
2678
|
+
state.inputMode = "p2p";
|
|
2679
|
+
state.inputBuffer = state.p2pPassword;
|
|
2680
|
+
render();
|
|
2681
|
+
}
|
|
2675
2682
|
return;
|
|
2683
|
+
case "q":
|
|
2684
|
+
stopLive();
|
|
2685
|
+
break;
|
|
2686
|
+
case "left": {
|
|
2687
|
+
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
2688
|
+
if (currentIdx > 0) {
|
|
2689
|
+
state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
|
|
2690
|
+
} else if (currentIdx === -1) {
|
|
2691
|
+
state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
|
|
2692
|
+
}
|
|
2693
|
+
setStatus(`Panel: ${state.activePanel}`);
|
|
2694
|
+
render();
|
|
2695
|
+
break;
|
|
2676
2696
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2697
|
+
case "right": {
|
|
2698
|
+
const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
|
|
2699
|
+
if (currentIdx < state.PANEL_TABS.length - 1) {
|
|
2700
|
+
state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
|
|
2701
|
+
} else if (currentIdx === -1) {
|
|
2702
|
+
state.activePanel = state.PANEL_TABS[0].id;
|
|
2703
|
+
}
|
|
2704
|
+
setStatus(`Panel: ${state.activePanel}`);
|
|
2705
|
+
render();
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
case "c":
|
|
2709
|
+
state.inputMode = "customizer";
|
|
2710
|
+
state.customizerIndex = 0;
|
|
2711
|
+
render();
|
|
2712
|
+
break;
|
|
2713
|
+
case "p":
|
|
2714
|
+
state.paused = !state.paused;
|
|
2715
|
+
setStatus(state.paused ? "Paused" : "Resumed");
|
|
2716
|
+
render();
|
|
2717
|
+
break;
|
|
2718
|
+
case "g":
|
|
2719
|
+
state.showGraphs = !state.showGraphs;
|
|
2720
|
+
render();
|
|
2721
|
+
break;
|
|
2722
|
+
case "b":
|
|
2723
|
+
state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
|
|
2724
|
+
setStatus(`Graph mode: ${state.graphMode}`);
|
|
2725
|
+
render();
|
|
2726
|
+
break;
|
|
2727
|
+
case "d":
|
|
2728
|
+
state.detailed = !state.detailed;
|
|
2729
|
+
setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
|
|
2730
|
+
break;
|
|
2731
|
+
case "s":
|
|
2732
|
+
const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
|
|
2733
|
+
const curIdx = sortCycle.indexOf(state.sortMode);
|
|
2734
|
+
state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
|
|
2735
|
+
setStatus(`Sorting by ${state.sortMode}`);
|
|
2736
|
+
render();
|
|
2737
|
+
break;
|
|
2738
|
+
case "k":
|
|
2739
|
+
state.inputMode = "kill";
|
|
2740
|
+
state.inputBuffer = "";
|
|
2741
|
+
render();
|
|
2742
|
+
break;
|
|
2743
|
+
case "S":
|
|
2744
|
+
state.inputMode = "signal";
|
|
2745
|
+
state.inputBuffer = "SIGTERM";
|
|
2746
|
+
render();
|
|
2747
|
+
break;
|
|
2748
|
+
case "t":
|
|
2749
|
+
state.treeView = !state.treeView;
|
|
2750
|
+
setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
|
|
2751
|
+
render();
|
|
2752
|
+
break;
|
|
2753
|
+
case "e":
|
|
2754
|
+
exportSnapshot();
|
|
2755
|
+
render();
|
|
2756
|
+
break;
|
|
2757
|
+
case "l":
|
|
2758
|
+
toggleLogging();
|
|
2759
|
+
render();
|
|
2760
|
+
break;
|
|
2761
|
+
case "f":
|
|
2762
|
+
state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
|
|
2763
|
+
setStatus(`Export format: ${state.exportFormat}`);
|
|
2764
|
+
render();
|
|
2765
|
+
break;
|
|
2766
|
+
case "tab":
|
|
2767
|
+
cycleTab(1);
|
|
2768
|
+
break;
|
|
2769
|
+
case "left":
|
|
2770
|
+
cycleTab(-1);
|
|
2771
|
+
break;
|
|
2772
|
+
case "right":
|
|
2773
|
+
cycleTab(1);
|
|
2774
|
+
break;
|
|
2775
|
+
default:
|
|
2776
|
+
if (str && str.length === 1) {
|
|
2777
|
+
const tabId = tabKeyToId(str);
|
|
2778
|
+
if (tabId) {
|
|
2779
|
+
state.activePanel = tabId;
|
|
2780
|
+
setStatus(`Panel: ${tabId.toUpperCase()}`);
|
|
2781
|
+
render();
|
|
2708
2782
|
}
|
|
2709
|
-
|
|
2710
|
-
case "pong":
|
|
2711
|
-
break;
|
|
2712
|
-
case "disconnect":
|
|
2713
|
-
const disconnectPayload = msg.payload;
|
|
2714
|
-
this.status(`Server disconnected: ${disconnectPayload.reason || "unknown"}`);
|
|
2715
|
-
this.disconnect();
|
|
2716
|
-
break;
|
|
2717
|
-
default:
|
|
2718
|
-
break;
|
|
2719
|
-
}
|
|
2720
|
-
} catch {
|
|
2783
|
+
}
|
|
2721
2784
|
}
|
|
2785
|
+
};
|
|
2786
|
+
process.stdin.on("keypress", state.keypressHandler);
|
|
2787
|
+
process.once("SIGINT", () => stopLive());
|
|
2788
|
+
}
|
|
2789
|
+
function tabKeyToId(str) {
|
|
2790
|
+
const map = { "1": "cpu", "2": "mem", "3": "gpu", "4": "power", "5": "battery", "6": "thermal", "7": "network", "8": "packets", "9": "tasks", "0": "disk", "p": "process", "r": "p2p" };
|
|
2791
|
+
return map[str] ?? null;
|
|
2792
|
+
}
|
|
2793
|
+
function cycleTab(direction) {
|
|
2794
|
+
const tabs = state.PANEL_TABS.map((t) => t.id);
|
|
2795
|
+
const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
|
|
2796
|
+
const next = (current + direction + tabs.length) % tabs.length;
|
|
2797
|
+
state.activePanel = tabs[next];
|
|
2798
|
+
setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
|
|
2799
|
+
render();
|
|
2800
|
+
}
|
|
2801
|
+
function handleNormalMouse(seq) {
|
|
2802
|
+
if (seq.length < 6) return;
|
|
2803
|
+
const cb = seq.charCodeAt(3) - 32;
|
|
2804
|
+
const cx = seq.charCodeAt(4) - 32;
|
|
2805
|
+
const cy = seq.charCodeAt(5) - 32;
|
|
2806
|
+
if (cb === 0 || cb === 1) {
|
|
2807
|
+
handleMouseClick(cy, cx);
|
|
2722
2808
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2809
|
+
}
|
|
2810
|
+
function handleSGRMouse(seq) {
|
|
2811
|
+
const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
2812
|
+
if (!match) return;
|
|
2813
|
+
const button = parseInt(match[1]);
|
|
2814
|
+
const cx = parseInt(match[2]);
|
|
2815
|
+
const cy = parseInt(match[3]);
|
|
2816
|
+
if (button === 0 || button === 1) {
|
|
2817
|
+
handleMouseClick(cy, cx);
|
|
2727
2818
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2819
|
+
}
|
|
2820
|
+
function handleMouseClick(y, x) {
|
|
2821
|
+
if (y !== TAB_BAR_ROW) return;
|
|
2822
|
+
const hitboxes = getTabHitboxes();
|
|
2823
|
+
const hit = hitboxes.find((h) => x >= h.start && x < h.end);
|
|
2824
|
+
if (hit) {
|
|
2825
|
+
state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
|
|
2826
|
+
setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
|
|
2827
|
+
render();
|
|
2734
2828
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
});
|
|
2741
|
-
}, 3e3);
|
|
2742
|
-
}
|
|
2829
|
+
}
|
|
2830
|
+
function stopLive() {
|
|
2831
|
+
if (state.intervalHandle) {
|
|
2832
|
+
clearInterval(state.intervalHandle);
|
|
2833
|
+
state.intervalHandle = null;
|
|
2743
2834
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
this.status("Connection refused \u2014 is the server running?");
|
|
2748
|
-
} else if (code === "ETIMEDOUT") {
|
|
2749
|
-
this.status("Connection timed out");
|
|
2750
|
-
} else if (code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") {
|
|
2751
|
-
this.status("TLS certificate verification failed \u2014 use --p2p-insecure to skip");
|
|
2752
|
-
} else {
|
|
2753
|
-
this.status(`Connection error: ${err.message}`);
|
|
2754
|
-
}
|
|
2835
|
+
if (state.statusTimer) {
|
|
2836
|
+
clearTimeout(state.statusTimer);
|
|
2837
|
+
state.statusTimer = null;
|
|
2755
2838
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2839
|
+
if (state.logStream) {
|
|
2840
|
+
state.logStream.end();
|
|
2841
|
+
state.logStream = null;
|
|
2759
2842
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2843
|
+
if (state.p2pServer) {
|
|
2844
|
+
state.p2pServer.stop();
|
|
2845
|
+
state.p2pServer = null;
|
|
2846
|
+
state.p2pServerRunning = false;
|
|
2764
2847
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
const ip = this.options.host;
|
|
2769
|
-
this.auditStream.write(`${ts} ${ip} ${event} ${detail}
|
|
2770
|
-
`);
|
|
2848
|
+
if (state.keypressHandler) {
|
|
2849
|
+
process.stdin.removeListener("keypress", state.keypressHandler);
|
|
2850
|
+
state.keypressHandler = null;
|
|
2771
2851
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2852
|
+
process.stdout.removeListener("resize", onResize);
|
|
2853
|
+
state.running = false;
|
|
2854
|
+
state.logging = false;
|
|
2855
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
2856
|
+
process.stdin.pause();
|
|
2857
|
+
if (state.mouseEnabled) {
|
|
2858
|
+
process.stdout.write("\x1B[?1000l");
|
|
2859
|
+
}
|
|
2860
|
+
process.stdout.write("\x1B[?25h");
|
|
2861
|
+
process.stdout.write("\x1B[?1049l");
|
|
2862
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2863
|
+
process.exit(0);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
// src/splash.ts
|
|
2867
|
+
import chalk8 from "chalk";
|
|
2868
|
+
var MAXV = 36;
|
|
2869
|
+
var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
|
|
2870
|
+
var palette = [
|
|
2871
|
+
"#0a0505",
|
|
2872
|
+
"#1a0806",
|
|
2873
|
+
"#2a0b06",
|
|
2874
|
+
"#3f1004",
|
|
2875
|
+
"#591404",
|
|
2876
|
+
"#791804",
|
|
2877
|
+
"#9e1c04",
|
|
2878
|
+
"#c92004",
|
|
2879
|
+
"#d63004",
|
|
2880
|
+
"#e34004",
|
|
2881
|
+
"#f05004",
|
|
2882
|
+
"#fd6004",
|
|
2883
|
+
"#ff7004",
|
|
2884
|
+
"#ff8004",
|
|
2885
|
+
"#ff9004",
|
|
2886
|
+
"#ffa004",
|
|
2887
|
+
"#ffb004",
|
|
2888
|
+
"#ffc004",
|
|
2889
|
+
"#ffd004",
|
|
2890
|
+
"#ffe004",
|
|
2891
|
+
"#fff004",
|
|
2892
|
+
"#fff820",
|
|
2893
|
+
"#ffff40",
|
|
2894
|
+
"#ffff60",
|
|
2895
|
+
"#ffff80",
|
|
2896
|
+
"#ffffa0",
|
|
2897
|
+
"#ffffc0",
|
|
2898
|
+
"#ffffe0",
|
|
2899
|
+
"#ffffff",
|
|
2900
|
+
"#ffffff",
|
|
2901
|
+
"#ffffff",
|
|
2902
|
+
"#ffffff",
|
|
2903
|
+
"#ffffff",
|
|
2904
|
+
"#ffffff",
|
|
2905
|
+
"#ffffff",
|
|
2906
|
+
"#ffffff"
|
|
2907
|
+
];
|
|
2908
|
+
function getRgb(hex) {
|
|
2909
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
2910
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
2911
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
2912
|
+
return { r, g, b };
|
|
2913
|
+
}
|
|
2914
|
+
var rgbPalette = palette.map(getRgb);
|
|
2915
|
+
function getChar(val) {
|
|
2916
|
+
const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
|
|
2917
|
+
return CHAR_RAMP[idx];
|
|
2918
|
+
}
|
|
2919
|
+
async function showSplash() {
|
|
2920
|
+
const cols = 60;
|
|
2921
|
+
const rows = 15;
|
|
2922
|
+
let fire = new Uint8Array(cols * rows);
|
|
2923
|
+
process.stdout.write("\x1B[2J");
|
|
2924
|
+
const termRows = process.stdout.rows || 24;
|
|
2925
|
+
const termCols = process.stdout.columns || 80;
|
|
2926
|
+
const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
|
|
2927
|
+
const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
|
|
2928
|
+
return new Promise((resolve) => {
|
|
2929
|
+
const interval = setInterval(() => {
|
|
2930
|
+
for (let x = 0; x < cols; x++) {
|
|
2931
|
+
fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
|
|
2932
|
+
}
|
|
2933
|
+
for (let y = rows - 1; y >= 1; y--) {
|
|
2934
|
+
for (let x = 0; x < cols; x++) {
|
|
2935
|
+
const src = y * cols + x;
|
|
2936
|
+
const val = fire[src];
|
|
2937
|
+
if (val === 0) {
|
|
2938
|
+
fire[(y - 1) * cols + x] = 0;
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2941
|
+
const decay = Math.floor(Math.random() * 3);
|
|
2942
|
+
const drift = Math.floor(Math.random() * 3) - 1;
|
|
2943
|
+
let nx = x + drift;
|
|
2944
|
+
if (nx < 0) nx = 0;
|
|
2945
|
+
else if (nx >= cols) nx = cols - 1;
|
|
2946
|
+
fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
let output = "";
|
|
2950
|
+
for (let i = 0; i < startRow; i++) output += "\n";
|
|
2951
|
+
for (let y = 0; y < rows; y++) {
|
|
2952
|
+
output += " ".repeat(startCol);
|
|
2953
|
+
for (let x = 0; x < cols; x++) {
|
|
2954
|
+
const v = fire[y * cols + x];
|
|
2955
|
+
const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
|
|
2956
|
+
output += chalk8.rgb(color.r, color.g, color.b)(getChar(v));
|
|
2957
|
+
}
|
|
2958
|
+
output += "\n";
|
|
2959
|
+
}
|
|
2960
|
+
output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk8.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
|
|
2961
|
+
output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk8.dim("[Press Enter to continue]");
|
|
2962
|
+
process.stdout.write(`\x1B[H${output}`);
|
|
2963
|
+
}, 65);
|
|
2964
|
+
const onEnter = () => {
|
|
2965
|
+
process.stdin.removeListener("data", onEnter);
|
|
2966
|
+
clearInterval(interval);
|
|
2967
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2968
|
+
resolve();
|
|
2969
|
+
};
|
|
2970
|
+
process.stdin.setRawMode(true);
|
|
2971
|
+
process.stdin.resume();
|
|
2972
|
+
process.stdin.on("data", onEnter);
|
|
2973
|
+
setTimeout(onEnter, 5e3);
|
|
2974
|
+
});
|
|
2775
2975
|
}
|
|
2776
2976
|
|
|
2777
2977
|
// src/index.ts
|
|
@@ -2791,14 +2991,14 @@ async function main() {
|
|
|
2791
2991
|
if (cmd === "live") {
|
|
2792
2992
|
program.args.shift();
|
|
2793
2993
|
}
|
|
2794
|
-
|
|
2994
|
+
const splashPromise = showSplash();
|
|
2795
2995
|
const interval = parseFloat(opts.interval) || 2;
|
|
2796
2996
|
await startLive({
|
|
2797
2997
|
interval,
|
|
2798
2998
|
detailed: opts.detailed,
|
|
2799
2999
|
exportDir: opts.exportDir,
|
|
2800
3000
|
autoLog: opts.log
|
|
2801
|
-
});
|
|
3001
|
+
}, splashPromise);
|
|
2802
3002
|
return;
|
|
2803
3003
|
}
|
|
2804
3004
|
if (cmd === "p2p") {
|