pyre-cli 3.0.0 → 3.1.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/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: "p" }
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
- lines.push(
1594
- formatTable(state.lastData, {
1595
- width: state.termWidth,
1596
- sortBy: state.sortMode,
1597
- filter: state.processFilter || void 0,
1598
- processLimit: processRowBudget(),
1599
- theme: state.currentTheme,
1600
- visible: state.visiblePanels,
1601
- treeView: state.treeView,
1602
- activePanel: state.activePanel
1603
- })
1604
- );
1605
- if (state.showGraphs) {
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,1242 @@ function checkAlerts(data) {
1667
1708
  }
1668
1709
  }
1669
1710
 
1670
- // src/live/input.ts
1671
- function handleInputModeKey(str, key) {
1672
- if (key.name === "escape") {
1673
- state.inputMode = null;
1674
- state.inputBuffer = "";
1675
- render();
1676
- return;
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
- if (state.inputMode === "customizer") {
1679
- const themesList = Object.keys(THEMES);
1680
- if (key.name === "up" || str === "k") {
1681
- state.customizerIndex = (state.customizerIndex - 1 + state.CUSTOMIZER_OPTIONS.length) % state.CUSTOMIZER_OPTIONS.length;
1682
- } else if (key.name === "down" || str === "j") {
1683
- state.customizerIndex = (state.customizerIndex + 1) % state.CUSTOMIZER_OPTIONS.length;
1684
- } else if (key.name === "return" || str === " ") {
1685
- const selected = state.CUSTOMIZER_OPTIONS[state.customizerIndex];
1686
- if (selected === "Theme") {
1687
- const nextIdx = (themesList.indexOf(state.currentTheme) + 1) % themesList.length;
1688
- state.currentTheme = themesList[nextIdx];
1689
- } else if (selected === "Toggle Tree View") {
1690
- state.treeView = !state.treeView;
1691
- } else if (selected === "Graph Mode") {
1692
- state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
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
- const itemKey = selected.replace("Toggle ", "").toLowerCase();
1695
- state.visiblePanels[itemKey] = !state.visiblePanels[itemKey];
1782
+ break;
1696
1783
  }
1697
1784
  }
1698
- render();
1699
- return;
1700
- }
1701
- if (state.inputMode === "signal") {
1702
- if (key.name === "up" || str === "k") {
1703
- const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
1704
- state.inputBuffer = state.SIGNAL_OPTIONS[(idx - 1 + state.SIGNAL_OPTIONS.length) % state.SIGNAL_OPTIONS.length];
1705
- } else if (key.name === "down" || str === "j") {
1706
- const idx = state.SIGNAL_OPTIONS.indexOf(state.inputBuffer);
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
- if (key.name === "return") {
1719
- if (state.inputMode === "filter") {
1720
- state.processFilter = state.inputBuffer.trim();
1721
- setStatus(state.processFilter ? `Filtering: "${state.processFilter}"` : "Filter cleared");
1722
- } else if (state.inputMode === "kill") {
1723
- killProcess(state.inputBuffer.trim());
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
- function killProcess(pidStr, signal = "SIGTERM") {
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
- try {
1747
- process.kill(pid, signal);
1748
- setStatus(`Sent ${signal} to PID ${pid}`);
1749
- } catch (err) {
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 onResize() {
1754
- state.termWidth = process.stdout.columns || 80;
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
- async function tick() {
1764
- if (state.paused) return;
1765
- try {
1766
- const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
1767
- state.lastData = data;
1768
- const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
1769
- state.history.push({
1770
- cpuUsage: data.cpu.usage,
1771
- memUsage: data.memory.usagePercent,
1772
- temp,
1773
- rxBytes: data.network.rxBytes,
1774
- txBytes: data.network.txBytes,
1775
- gpuUtil: data.gpu?.utilization,
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
- async function startLive(opts2) {
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
- await tick();
1806
- restartTicker();
1807
- if (opts2.autoLog) startLogging();
1808
- readline.emitKeypressEvents(process.stdin);
1809
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
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
- if (key.mouse) {
1823
- handleMouseClick(key.mouse);
1824
- return;
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
- if (key.sequence && key.sequence.startsWith("\x1B[M")) {
1827
- handleNormalMouse(key.sequence);
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 (key.sequence && key.sequence.startsWith("\x1B[<")) {
1831
- handleSGRMouse(key.sequence);
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
- switch (key.sequence) {
1835
- case "+":
1836
- state.interval += 1;
1837
- restartTicker();
1838
- setStatus(`Interval set to ${state.interval}s`);
1839
- return;
1840
- case "-":
1841
- state.interval = Math.max(1, state.interval - 1);
1842
- restartTicker();
1843
- setStatus(`Interval set to ${state.interval}s`);
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
- case "/":
1846
- state.inputMode = "filter";
1847
- state.inputBuffer = state.processFilter;
1848
- render();
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
- case "0":
1851
- case "1":
1852
- case "2":
1853
- case "3":
1854
- case "4":
1855
- case "5":
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
- case "P": {
1881
- state.activePanel = state.activePanel === "process" ? "grid" : "process";
1882
- setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
1883
- render();
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
- switch (key.name) {
1888
- case "q":
1889
- stopLive();
1890
- break;
1891
- case "left": {
1892
- const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
1893
- if (currentIdx > 0) {
1894
- state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
1895
- } else if (currentIdx === -1) {
1896
- state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
1897
- }
1898
- setStatus(`Panel: ${state.activePanel}`);
1899
- render();
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
- case "right": {
1903
- const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
1904
- if (currentIdx < state.PANEL_TABS.length - 1) {
1905
- state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
1906
- } else if (currentIdx === -1) {
1907
- state.activePanel = state.PANEL_TABS[0].id;
1908
- }
1909
- setStatus(`Panel: ${state.activePanel}`);
1910
- render();
1911
- break;
1912
- }
1913
- case "c":
1914
- state.inputMode = "customizer";
1915
- state.customizerIndex = 0;
1916
- render();
1917
- break;
1918
- case "p":
1919
- state.paused = !state.paused;
1920
- setStatus(state.paused ? "Paused" : "Resumed");
1921
- render();
1922
- break;
1923
- case "g":
1924
- state.showGraphs = !state.showGraphs;
1925
- render();
1926
- break;
1927
- case "b":
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
- }
1989
- }
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);
2013
- }
2014
- }
2015
- function handleSGRMouse(seq) {
2016
- const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
2017
- if (!match) return;
2018
- const button = parseInt(match[1]);
2019
- const cx = parseInt(match[2]);
2020
- const cy = parseInt(match[3]);
2021
- if (button === 0 || button === 1) {
2022
- handleMouseClick(cy, cx);
2023
- }
2024
- }
2025
- function handleMouseClick(y, x) {
2026
- if (y !== TAB_BAR_ROW) return;
2027
- const hitboxes = getTabHitboxes();
2028
- const hit = hitboxes.find((h) => x >= h.start && x < h.end);
2029
- if (hit) {
2030
- state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
2031
- setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
2032
- render();
2033
- }
2034
- }
2035
- function stopLive() {
2036
- if (state.intervalHandle) {
2037
- clearInterval(state.intervalHandle);
2038
- state.intervalHandle = null;
2039
- }
2040
- if (state.statusTimer) {
2041
- clearTimeout(state.statusTimer);
2042
- state.statusTimer = null;
2043
- }
2044
- if (state.logStream) {
2045
- state.logStream.end();
2046
- state.logStream = null;
2047
- }
2048
- if (state.keypressHandler) {
2049
- process.stdin.removeListener("keypress", state.keypressHandler);
2050
- state.keypressHandler = null;
2068
+ };
2069
+ send();
2070
+ peer.intervalHandle = setInterval(send, this.options.intervalMs);
2051
2071
  }
2052
- process.stdout.removeListener("resize", onResize);
2053
- state.running = false;
2054
- state.logging = false;
2055
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
2056
- process.stdin.pause();
2057
- if (state.mouseEnabled) {
2058
- process.stdout.write("\x1B[?1000l");
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);
2059
2077
  }
2060
- process.stdout.write("\x1B[?25h");
2061
- process.stdout.write("\x1B[?1049l");
2062
- process.stdout.write("\x1B[2J\x1B[H");
2063
- process.exit(0);
2064
- }
2065
-
2066
- // src/splash.ts
2067
- import chalk6 from "chalk";
2068
- var MAXV = 36;
2069
- var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
2070
- var palette = [
2071
- "#0a0505",
2072
- "#1a0806",
2073
- "#2a0b06",
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);
2132
- }
2133
- for (let y = rows - 1; y >= 1; y--) {
2134
- for (let x = 0; x < cols; x++) {
2135
- const src = y * cols + x;
2136
- const val = fire[src];
2137
- if (val === 0) {
2138
- fire[(y - 1) * cols + x] = 0;
2139
- continue;
2140
- }
2141
- const decay = Math.floor(Math.random() * 3);
2142
- const drift = Math.floor(Math.random() * 3) - 1;
2143
- let nx = x + drift;
2144
- if (nx < 0) nx = 0;
2145
- else if (nx >= cols) nx = cols - 1;
2146
- fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
2147
- }
2148
- }
2149
- let output = "";
2150
- for (let i = 0; i < startRow; i++) output += "\n";
2151
- for (let y = 0; y < rows; y++) {
2152
- output += " ".repeat(startCol);
2153
- for (let x = 0; x < cols; x++) {
2154
- const v = fire[y * cols + x];
2155
- const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
2156
- output += chalk6.rgb(color.r, color.g, color.b)(getChar(v));
2157
- }
2158
- output += "\n";
2159
- }
2160
- output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk6.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
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;
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");
2218
2083
  }
2219
- }
2220
- function encodeDisconnect(reason) {
2221
- return buildMessage("disconnect", { reason });
2222
- }
2223
- function decodeMessage(buf) {
2224
- const str = buf.toString("utf-8");
2225
- const msg = JSON.parse(str);
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;
2249
- }
2250
- }
2251
- if (!peer.readExpectingHeader) {
2252
- if (peer.readBuffer.length >= peer.readMessageLength) {
2253
- const body = peer.readBuffer.subarray(0, peer.readMessageLength);
2254
- peer.readBuffer = peer.readBuffer.subarray(peer.readMessageLength);
2255
- peer.readExpectingHeader = true;
2256
- return body;
2257
- } else {
2258
- break;
2259
- }
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);
2260
2094
  }
2095
+ this.cleanupPeer(peer);
2261
2096
  }
2262
- return null;
2263
- }
2264
- function isIPAllowed(ip, allowedIPs, deniedIPs) {
2265
- for (const denied of deniedIPs) {
2266
- if (ip === denied || ip.startsWith(denied)) {
2267
- return false;
2097
+ disconnectPeer(peer, reason) {
2098
+ try {
2099
+ this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
2100
+ } catch {
2268
2101
  }
2102
+ peer.socket.destroy();
2269
2103
  }
2270
- if (allowedIPs.length === 0) {
2271
- return true;
2272
- }
2273
- for (const allowed of allowedIPs) {
2274
- if (ip === allowed || ip.startsWith(allowed)) {
2275
- return true;
2104
+ cleanupPeer(peer) {
2105
+ if (peer.intervalHandle) {
2106
+ clearInterval(peer.intervalHandle);
2107
+ peer.intervalHandle = null;
2276
2108
  }
2109
+ if (peer.pingHandle) {
2110
+ clearInterval(peer.pingHandle);
2111
+ peer.pingHandle = null;
2112
+ }
2113
+ this.peers.delete(peer.socket);
2277
2114
  }
2278
- return false;
2279
- }
2280
- function deriveHMACKey(password) {
2281
- return crypto2.createHash("sha256").update(password).digest("hex").slice(0, HMAC_KEY_LENGTH);
2115
+ };
2116
+ async function startP2PServer(options) {
2117
+ const server = new P2PServer(options);
2118
+ await server.start();
2119
+ return server;
2282
2120
  }
2283
- var P2PServer = class {
2284
- server = null;
2285
- peers = /* @__PURE__ */ new Map();
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;
2286
2130
  options;
2287
2131
  running = false;
2288
- rateLimitMap = /* @__PURE__ */ new Map();
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;
2289
2138
  auditStream = null;
2290
2139
  hmacKey;
2291
2140
  constructor(options) {
2292
2141
  this.options = options;
2293
- this.hmacKey = deriveHMACKey(options.password);
2142
+ this.hmacKey = deriveHMACKey2(options.password);
2294
2143
  }
2295
- get info() {
2296
- return {
2297
- host: this.options.host,
2298
- port: this.options.port,
2299
- peersConnected: this.peers.size
2300
- };
2144
+ setOnData(cb) {
2145
+ this.onDataCallback = cb;
2301
2146
  }
2302
- async start() {
2147
+ setOnStatus(cb) {
2148
+ this.onStatusCallback = cb;
2149
+ }
2150
+ async connect() {
2303
2151
  if (this.options.auditLog) {
2304
2152
  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" });
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" });
2308
2156
  } catch {
2309
2157
  console.log(chalk7.yellow("Warning: could not open audit log"));
2310
2158
  }
2311
2159
  }
2312
2160
  return new Promise((resolve, reject) => {
2313
- const useTLS = !!(this.options.tlsCert && this.options.tlsKey);
2161
+ const useTLS = !!this.options.tls;
2314
2162
  if (useTLS) {
2315
2163
  const tlsOptions = {
2316
- key: fs2.readFileSync(this.options.tlsKey),
2317
- cert: fs2.readFileSync(this.options.tlsCert)
2164
+ host: this.options.host,
2165
+ port: this.options.port,
2166
+ rejectUnauthorized: !this.options.tlsInsecure
2318
2167
  };
2319
2168
  if (this.options.tlsCA) {
2320
- tlsOptions.ca = fs2.readFileSync(this.options.tlsCA);
2169
+ tlsOptions.ca = fs3.readFileSync(this.options.tlsCA);
2321
2170
  }
2322
- this.server = tls.createServer(tlsOptions, (socket) => this.onConnection(socket));
2171
+ this.socket = tls2.connect(tlsOptions, () => {
2172
+ this.status("Connecting...");
2173
+ });
2323
2174
  } else {
2324
- this.server = net.createServer((socket) => this.onConnection(socket));
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
+ });
2325
2187
  }
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
- });
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());
2341
2192
  });
2342
2193
  }
2343
- stop() {
2194
+ disconnect() {
2344
2195
  this.running = false;
2345
- for (const peer of this.peers.values()) {
2346
- this.disconnectPeer(peer, "Server shutting down");
2196
+ if (this.reconnectTimer) {
2197
+ clearTimeout(this.reconnectTimer);
2198
+ this.reconnectTimer = null;
2347
2199
  }
2348
- this.peers.clear();
2349
- this.rateLimitMap.clear();
2350
- if (this.server) {
2351
- this.server.close();
2352
- this.server = null;
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 {
2208
+ }
2209
+ this.socket.destroy();
2210
+ this.socket = null;
2353
2211
  }
2354
2212
  if (this.auditStream) {
2355
2213
  this.auditStream.end();
2356
2214
  this.auditStream = null;
2357
2215
  }
2216
+ this.status("Disconnected");
2358
2217
  }
2359
- audit(ip, event, detail) {
2360
- if (!this.auditStream) return;
2361
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2362
- this.auditStream.write(`${ts} ${ip} ${event} ${detail}
2363
- `);
2364
- }
2365
- getPeerIP(socket) {
2366
- const addr = socket.remoteAddress;
2367
- if (!addr) return "unknown";
2368
- if (addr.startsWith("::ffff:")) {
2369
- return addr.slice(7);
2370
- }
2371
- return addr;
2372
- }
2373
- checkRateLimit(ip) {
2374
- const maxAttempts = this.options.rateLimitMaxAttempts ?? DEFAULT_RATE_LIMIT_MAX;
2375
- const windowMs = this.options.rateLimitWindowMs ?? DEFAULT_RATE_LIMIT_WINDOW;
2376
- const now = Date.now();
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);
2394
- }
2395
- onConnection(socket) {
2396
- const ip = this.getPeerIP(socket);
2397
- const allowedIPs = this.options.allowedIPs ?? [];
2398
- const deniedIPs = this.options.deniedIPs ?? [];
2399
- if (!isIPAllowed(ip, allowedIPs, deniedIPs)) {
2400
- this.audit(ip, "BLOCKED", "IP not allowed");
2401
- console.log(chalk7.yellow(`Blocked IP: ${ip}`));
2402
- socket.destroy();
2403
- return;
2404
- }
2405
- if (!this.checkRateLimit(ip)) {
2406
- this.audit(ip, "RATE_LIMITED", "Too many auth attempts");
2407
- console.log(chalk7.yellow(`Rate limit exceeded for IP: ${ip}`));
2408
- socket.destroy();
2409
- return;
2410
- }
2411
- const peer = {
2412
- socket,
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");
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;
2228
+ }
2433
2229
  }
2434
- });
2435
- const nonce = generateNonce();
2436
- this.sendSigned(peer, signMessage("challenge", { nonce }, this.hmacKey));
2437
- peer.pendingNonce = nonce;
2438
- }
2439
- onData(peer, data) {
2440
- peer.readBuffer = Buffer.concat([peer.readBuffer, data]);
2441
- if (!peer.authenticated) {
2442
- const body = readMessageFromPeer(peer);
2443
- if (body) {
2444
- this.handleAuthMessage(peer, body);
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;
2238
+ }
2445
2239
  }
2446
- return;
2447
2240
  }
2448
2241
  }
2449
- handleAuthMessage(peer, body) {
2242
+ handleMessage(body) {
2450
2243
  try {
2451
2244
  const raw = body.toString("utf-8");
2452
2245
  const msg = decodeMessage(body);
2453
- if (msg.kind !== "auth") {
2454
- peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Expected auth message first" }, this.hmacKey));
2455
- this.disconnectPeer(peer, "Expected auth message");
2456
- return;
2457
- }
2458
- const authPayload = msg.payload;
2459
- if (!authPayload.passwordHash || !authPayload.nonce) {
2460
- peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid auth payload" }, this.hmacKey));
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");
2246
+ if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
2247
+ this.status("Message integrity check failed");
2468
2248
  return;
2469
2249
  }
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;
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;
2476
2292
  }
2477
- peer.authenticated = true;
2478
- this.cleanupRateLimit(peer.ip);
2479
- peer.pendingNonce = null;
2480
- peer.socket.write(signMessage("auth-ok", { ok: true }, this.hmacKey));
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
2293
  } catch {
2486
- peer.socket.write(signMessage("auth-fail", { ok: false, reason: "Invalid message format" }, this.hmacKey));
2487
- this.disconnectPeer(peer, "Invalid message format");
2488
2294
  }
2489
2295
  }
2490
- sendSigned(peer, message) {
2491
- if (!peer.socket.destroyed) {
2492
- peer.socket.write(message);
2296
+ sendSigned(message) {
2297
+ if (this.socket && !this.socket.destroyed) {
2298
+ this.socket.write(message);
2493
2299
  }
2494
2300
  }
2495
- startDataStream(peer) {
2496
- const send = async () => {
2497
- if (!peer.authenticated || peer.socket.destroyed) return;
2498
- try {
2499
- const data = await collectAll({ detailed: this.options.detailed });
2500
- this.sendSigned(peer, signMessage("data", { stats: data }, this.hmacKey));
2501
- } catch {
2502
- }
2503
- };
2504
- send();
2505
- peer.intervalHandle = setInterval(send, this.options.intervalMs);
2506
- }
2507
- startPing(peer) {
2508
- peer.pingHandle = setInterval(() => {
2509
- if (peer.socket.destroyed) return;
2510
- this.sendSigned(peer, signMessage("ping", { ts: Date.now() }, this.hmacKey));
2511
- }, 5e3);
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);
2512
2307
  }
2513
- onPeerClose(peer) {
2514
- this.cleanupPeer(peer);
2515
- console.log(chalk7.yellow(`Peer disconnected: ${peer.socket.remoteAddress}`));
2516
- this.audit(peer.ip, "DISCONNECT", "Peer closed connection");
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);
2315
+ }
2517
2316
  }
2518
- onPeerError(peer, err) {
2317
+ onError(err) {
2519
2318
  const code = err.code;
2520
- if (code === "ECONNRESET") {
2521
- console.log(chalk7.yellow(`Peer connection reset: ${peer.socket.remoteAddress}`));
2522
- this.audit(peer.ip, "ERROR", "Connection reset");
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");
2523
2325
  } else {
2524
- console.log(chalk7.red(`Peer error: ${err.message}`));
2525
- this.audit(peer.ip, "ERROR", err.message);
2326
+ this.status(`Connection error: ${err.message}`);
2526
2327
  }
2527
- this.cleanupPeer(peer);
2528
2328
  }
2529
- disconnectPeer(peer, reason) {
2530
- try {
2531
- this.sendSigned(peer, signMessage("disconnect", { reason }, this.hmacKey));
2532
- } catch {
2533
- }
2534
- peer.socket.destroy();
2329
+ onTimeout() {
2330
+ this.status("Connection timed out");
2331
+ this.disconnect();
2535
2332
  }
2536
- cleanupPeer(peer) {
2537
- if (peer.intervalHandle) {
2538
- clearInterval(peer.intervalHandle);
2539
- peer.intervalHandle = null;
2540
- }
2541
- if (peer.pingHandle) {
2542
- clearInterval(peer.pingHandle);
2543
- peer.pingHandle = null;
2333
+ status(msg) {
2334
+ if (this.onStatusCallback) {
2335
+ this.onStatusCallback(msg);
2544
2336
  }
2545
- this.peers.delete(peer.socket);
2337
+ }
2338
+ audit(event, detail) {
2339
+ if (!this.auditStream) return;
2340
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2341
+ const ip = this.options.host;
2342
+ this.auditStream.write(`${ts} ${ip} ${event} ${detail}
2343
+ `);
2546
2344
  }
2547
2345
  };
2346
+ function deriveHMACKey2(password) {
2347
+ return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
2348
+ }
2548
2349
 
2549
- // src/p2p/client.ts
2550
- import net2 from "net";
2551
- import tls2 from "tls";
2552
- import fs3 from "fs";
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);
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;
2570
2357
  }
2571
- setOnData(cb) {
2572
- this.onDataCallback = cb;
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];
2376
+ }
2377
+ }
2378
+ render();
2379
+ return;
2573
2380
  }
2574
- setOnStatus(cb) {
2575
- this.onStatusCallback = cb;
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();
2393
+ return;
2394
+ }
2395
+ render();
2396
+ return;
2576
2397
  }
2577
- async connect() {
2578
- if (this.options.auditLog) {
2579
- try {
2580
- fs3.mkdirSync(this.options.auditLog, { recursive: true });
2581
- const logFile = `${this.options.auditLog}/p2p-client-audit.log`;
2582
- this.auditStream = fs3.createWriteStream(logFile, { flags: "a" });
2583
- } catch {
2584
- console.log(chalk8.yellow("Warning: could not open audit log"));
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();
2585
2410
  }
2411
+ state.inputMode = null;
2412
+ state.inputBuffer = "";
2413
+ render();
2414
+ return;
2586
2415
  }
2587
- return new Promise((resolve, reject) => {
2588
- const useTLS = !!this.options.tls;
2589
- if (useTLS) {
2590
- const tlsOptions = {
2591
- host: this.options.host,
2592
- port: this.options.port,
2593
- rejectUnauthorized: !this.options.tlsInsecure
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
- });
2416
+ if (key.name === "backspace") {
2417
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
2418
+ render();
2419
+ return;
2420
+ }
2421
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
2422
+ state.inputBuffer += str;
2423
+ render();
2424
+ }
2425
+ return;
2426
+ }
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;
2438
+ }
2439
+ if (key.name === "backspace") {
2440
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
2441
+ render();
2442
+ return;
2443
+ }
2444
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
2445
+ state.inputBuffer += str;
2446
+ render();
2447
+ }
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;
2454
+ }
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}`);
2460
+ }
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();
2614
2475
  }
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());
2619
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();
2489
+ }
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;
2496
+ }
2497
+ }
2498
+ function stopP2PServer() {
2499
+ if (!state.p2pServer) return;
2500
+ try {
2501
+ state.p2pServer.stop();
2502
+ } catch {
2503
+ }
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 tick() {
2519
+ if (state.paused) return;
2520
+ try {
2521
+ const data = await collectAll({ detailed: state.detailed, processLimit: processRowBudget() });
2522
+ state.lastData = data;
2523
+ const temp = data.cpu.temperature ?? data.thermal.temperatures?.cpu_die ?? null;
2524
+ state.history.push({
2525
+ cpuUsage: data.cpu.usage,
2526
+ memUsage: data.memory.usagePercent,
2527
+ temp,
2528
+ rxBytes: data.network.rxBytes,
2529
+ txBytes: data.network.txBytes,
2530
+ gpuUtil: data.gpu?.utilization,
2531
+ powerWatts: data.power?.combinedWatts ?? data.power?.cpuWatts ?? 0,
2532
+ rxPackets: data.network.rxPackets,
2533
+ txPackets: data.network.txPackets,
2534
+ connections: data.network.connections ?? 0
2535
+ });
2536
+ writeLogRow(data);
2537
+ checkAlerts(data);
2538
+ syncP2PEvents();
2539
+ render();
2540
+ } catch {
2620
2541
  }
2621
- disconnect() {
2622
- this.running = false;
2623
- if (this.reconnectTimer) {
2624
- clearTimeout(this.reconnectTimer);
2625
- this.reconnectTimer = null;
2542
+ }
2543
+ async function startLive(opts2) {
2544
+ if (state.running) return;
2545
+ state.running = true;
2546
+ state.paused = false;
2547
+ state.detailed = !!opts2.detailed;
2548
+ state.interval = opts2.interval;
2549
+ if (opts2.exportDir) state.exportDir = opts2.exportDir;
2550
+ state.termWidth = process.stdout.columns || 80;
2551
+ state.termHeight = process.stdout.rows || 24;
2552
+ state.history.reset();
2553
+ state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
2554
+ process.stdout.write("\x1B[?1049h");
2555
+ process.stdout.write("\x1B[?25l");
2556
+ process.stdout.write("\x1B[2J\x1B[H");
2557
+ process.title = "pyre";
2558
+ if (state.mouseEnabled) {
2559
+ process.stdout.write("\x1B[?1000h");
2560
+ }
2561
+ await tick();
2562
+ restartTicker();
2563
+ if (opts2.autoLog) startLogging();
2564
+ readline.emitKeypressEvents(process.stdin);
2565
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
2566
+ process.stdin.resume();
2567
+ process.stdout.on("resize", onResize);
2568
+ state.keypressHandler = (str, key) => {
2569
+ if (!key) return;
2570
+ if (key.ctrl && key.name === "c") {
2571
+ stopLive();
2572
+ return;
2626
2573
  }
2627
- if (this.pingHandle) {
2628
- clearInterval(this.pingHandle);
2629
- this.pingHandle = null;
2574
+ if (state.inputMode) {
2575
+ handleInputModeKey(str, key);
2576
+ return;
2630
2577
  }
2631
- if (this.socket) {
2632
- try {
2633
- this.sendSigned(encodeDisconnect("Client closing"));
2634
- } catch {
2635
- }
2636
- this.socket.destroy();
2637
- this.socket = null;
2578
+ if (key.mouse) {
2579
+ handleMouseClick(key.mouse);
2580
+ return;
2638
2581
  }
2639
- if (this.auditStream) {
2640
- this.auditStream.end();
2641
- this.auditStream = null;
2582
+ if (key.sequence && key.sequence.startsWith("\x1B[M")) {
2583
+ handleNormalMouse(key.sequence);
2584
+ return;
2642
2585
  }
2643
- this.status("Disconnected");
2644
- }
2645
- onData(data) {
2646
- this.readState.buffer = Buffer.concat([this.readState.buffer, data]);
2647
- while (this.readState.buffer.length > 0) {
2648
- if (this.readState.expectingHeader) {
2649
- if (this.readState.buffer.length >= 4) {
2650
- this.readState.messageLength = this.readState.buffer.readUInt32BE(0);
2651
- this.readState.buffer = this.readState.buffer.subarray(4);
2652
- this.readState.expectingHeader = false;
2653
- } else {
2654
- break;
2586
+ if (key.sequence && key.sequence.startsWith("\x1B[<")) {
2587
+ handleSGRMouse(key.sequence);
2588
+ return;
2589
+ }
2590
+ switch (key.sequence) {
2591
+ case "+":
2592
+ state.interval += 1;
2593
+ restartTicker();
2594
+ setStatus(`Interval set to ${state.interval}s`);
2595
+ return;
2596
+ case "-":
2597
+ state.interval = Math.max(1, state.interval - 1);
2598
+ restartTicker();
2599
+ setStatus(`Interval set to ${state.interval}s`);
2600
+ return;
2601
+ case "/":
2602
+ state.inputMode = "filter";
2603
+ state.inputBuffer = state.processFilter;
2604
+ render();
2605
+ return;
2606
+ case "0":
2607
+ case "1":
2608
+ case "2":
2609
+ case "3":
2610
+ case "4":
2611
+ case "5":
2612
+ case "6":
2613
+ case "7":
2614
+ case "8":
2615
+ case "9": {
2616
+ const tabMap = {
2617
+ "0": "disk",
2618
+ "1": "cpu",
2619
+ "2": "mem",
2620
+ "3": "gpu",
2621
+ "4": "power",
2622
+ "5": "battery",
2623
+ "6": "thermal",
2624
+ "7": "network",
2625
+ "8": "packets",
2626
+ "9": "tasks"
2627
+ };
2628
+ const panelId = tabMap[key.sequence];
2629
+ if (panelId) {
2630
+ state.activePanel = state.activePanel === panelId ? "grid" : panelId;
2631
+ setStatus(state.activePanel === "grid" ? "Grid view" : `${panelId} panel`);
2632
+ render();
2655
2633
  }
2634
+ return;
2656
2635
  }
2657
- if (!this.readState.expectingHeader) {
2658
- if (this.readState.buffer.length >= this.readState.messageLength) {
2659
- const body = this.readState.buffer.subarray(0, this.readState.messageLength);
2660
- this.readState.buffer = this.readState.buffer.subarray(this.readState.messageLength);
2661
- this.readState.expectingHeader = true;
2662
- this.handleMessage(body);
2663
- } else {
2664
- break;
2665
- }
2636
+ case "P": {
2637
+ state.activePanel = state.activePanel === "process" ? "grid" : "process";
2638
+ setStatus(state.activePanel === "grid" ? "Grid view" : "Process panel");
2639
+ render();
2640
+ return;
2666
2641
  }
2667
2642
  }
2668
- }
2669
- handleMessage(body) {
2670
- try {
2671
- const raw = body.toString("utf-8");
2672
- const msg = decodeMessage(body);
2673
- if (!verifyHMAC(raw, msg.hmac ?? "", this.hmacKey)) {
2674
- this.status("Message integrity check failed");
2643
+ switch (key.name) {
2644
+ case "r":
2645
+ if (state.p2pServerRunning) {
2646
+ stopP2PServer();
2647
+ setStatus("P2P server stopped");
2648
+ render();
2649
+ } else {
2650
+ state.inputMode = "p2p";
2651
+ state.inputBuffer = state.p2pPassword;
2652
+ render();
2653
+ }
2675
2654
  return;
2655
+ case "q":
2656
+ stopLive();
2657
+ break;
2658
+ case "left": {
2659
+ const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
2660
+ if (currentIdx > 0) {
2661
+ state.activePanel = state.PANEL_TABS[currentIdx - 1].id;
2662
+ } else if (currentIdx === -1) {
2663
+ state.activePanel = state.PANEL_TABS[state.PANEL_TABS.length - 1].id;
2664
+ }
2665
+ setStatus(`Panel: ${state.activePanel}`);
2666
+ render();
2667
+ break;
2676
2668
  }
2677
- switch (msg.kind) {
2678
- case "challenge":
2679
- const challengePayload = msg.payload;
2680
- if (challengePayload.nonce) {
2681
- this.pendingNonce = challengePayload.nonce;
2682
- const passwordHash = hashPassword(this.options.password, challengePayload.nonce);
2683
- this.sendSigned(signMessage("auth", { passwordHash, nonce: challengePayload.nonce }, this.hmacKey));
2684
- this.status("Authenticating...");
2685
- }
2686
- break;
2687
- case "auth-ok":
2688
- this.status("Authenticated \u2014 receiving live data");
2689
- this.startPing();
2690
- break;
2691
- case "auth-fail":
2692
- const failPayload = msg.payload;
2693
- this.status(`Authentication failed: ${failPayload.reason || "unknown"}`);
2694
- this.disconnect();
2695
- break;
2696
- case "data":
2697
- if (this.onDataCallback) {
2698
- const dataPayload = msg.payload;
2699
- if (dataPayload.stats) {
2700
- this.onDataCallback(dataPayload.stats);
2701
- }
2702
- }
2703
- break;
2704
- case "ping":
2705
- if (this.socket && !this.socket.destroyed) {
2706
- const ts = msg.payload.ts;
2707
- this.sendSigned(signMessage("pong", { ts }, this.hmacKey));
2669
+ case "right": {
2670
+ const currentIdx = state.PANEL_TABS.findIndex((t) => t.id === state.activePanel);
2671
+ if (currentIdx < state.PANEL_TABS.length - 1) {
2672
+ state.activePanel = state.PANEL_TABS[currentIdx + 1].id;
2673
+ } else if (currentIdx === -1) {
2674
+ state.activePanel = state.PANEL_TABS[0].id;
2675
+ }
2676
+ setStatus(`Panel: ${state.activePanel}`);
2677
+ render();
2678
+ break;
2679
+ }
2680
+ case "c":
2681
+ state.inputMode = "customizer";
2682
+ state.customizerIndex = 0;
2683
+ render();
2684
+ break;
2685
+ case "p":
2686
+ state.paused = !state.paused;
2687
+ setStatus(state.paused ? "Paused" : "Resumed");
2688
+ render();
2689
+ break;
2690
+ case "g":
2691
+ state.showGraphs = !state.showGraphs;
2692
+ render();
2693
+ break;
2694
+ case "b":
2695
+ state.graphMode = state.graphMode === "spark" ? "bar" : "spark";
2696
+ setStatus(`Graph mode: ${state.graphMode}`);
2697
+ render();
2698
+ break;
2699
+ case "d":
2700
+ state.detailed = !state.detailed;
2701
+ setStatus(`Detailed sensor mode: ${state.detailed ? "on" : "off"}`);
2702
+ break;
2703
+ case "s":
2704
+ const sortCycle = ["cpu", "mem", "pid", "user", "command", "state", "threads", "runtime"];
2705
+ const curIdx = sortCycle.indexOf(state.sortMode);
2706
+ state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
2707
+ setStatus(`Sorting by ${state.sortMode}`);
2708
+ render();
2709
+ break;
2710
+ case "k":
2711
+ state.inputMode = "kill";
2712
+ state.inputBuffer = "";
2713
+ render();
2714
+ break;
2715
+ case "S":
2716
+ state.inputMode = "signal";
2717
+ state.inputBuffer = "SIGTERM";
2718
+ render();
2719
+ break;
2720
+ case "t":
2721
+ state.treeView = !state.treeView;
2722
+ setStatus(state.treeView ? "Tree view enabled" : "Flat view enabled");
2723
+ render();
2724
+ break;
2725
+ case "e":
2726
+ exportSnapshot();
2727
+ render();
2728
+ break;
2729
+ case "l":
2730
+ toggleLogging();
2731
+ render();
2732
+ break;
2733
+ case "f":
2734
+ state.exportFormat = state.exportFormat === "json" ? "csv" : state.exportFormat === "csv" ? "tsv" : "json";
2735
+ setStatus(`Export format: ${state.exportFormat}`);
2736
+ render();
2737
+ break;
2738
+ case "tab":
2739
+ cycleTab(1);
2740
+ break;
2741
+ case "left":
2742
+ cycleTab(-1);
2743
+ break;
2744
+ case "right":
2745
+ cycleTab(1);
2746
+ break;
2747
+ default:
2748
+ if (str && str.length === 1) {
2749
+ const tabId = tabKeyToId(str);
2750
+ if (tabId) {
2751
+ state.activePanel = tabId;
2752
+ setStatus(`Panel: ${tabId.toUpperCase()}`);
2753
+ render();
2708
2754
  }
2709
- break;
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 {
2755
+ }
2721
2756
  }
2757
+ };
2758
+ process.stdin.on("keypress", state.keypressHandler);
2759
+ process.once("SIGINT", () => stopLive());
2760
+ }
2761
+ function tabKeyToId(str) {
2762
+ 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" };
2763
+ return map[str] ?? null;
2764
+ }
2765
+ function cycleTab(direction) {
2766
+ const tabs = state.PANEL_TABS.map((t) => t.id);
2767
+ const current = state.activePanel === "grid" ? -1 : tabs.indexOf(state.activePanel);
2768
+ const next = (current + direction + tabs.length) % tabs.length;
2769
+ state.activePanel = tabs[next];
2770
+ setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
2771
+ render();
2772
+ }
2773
+ function handleNormalMouse(seq) {
2774
+ if (seq.length < 6) return;
2775
+ const cb = seq.charCodeAt(3) - 32;
2776
+ const cx = seq.charCodeAt(4) - 32;
2777
+ const cy = seq.charCodeAt(5) - 32;
2778
+ if (cb === 0 || cb === 1) {
2779
+ handleMouseClick(cy, cx);
2722
2780
  }
2723
- sendSigned(message) {
2724
- if (this.socket && !this.socket.destroyed) {
2725
- this.socket.write(message);
2726
- }
2781
+ }
2782
+ function handleSGRMouse(seq) {
2783
+ const match = seq.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
2784
+ if (!match) return;
2785
+ const button = parseInt(match[1]);
2786
+ const cx = parseInt(match[2]);
2787
+ const cy = parseInt(match[3]);
2788
+ if (button === 0 || button === 1) {
2789
+ handleMouseClick(cy, cx);
2727
2790
  }
2728
- startPing() {
2729
- this.pingHandle = setInterval(() => {
2730
- if (this.socket && !this.socket.destroyed) {
2731
- this.sendSigned(signMessage("ping", { ts: Date.now() }, this.hmacKey));
2732
- }
2733
- }, 15e3);
2791
+ }
2792
+ function handleMouseClick(y, x) {
2793
+ if (y !== TAB_BAR_ROW) return;
2794
+ const hitboxes = getTabHitboxes();
2795
+ const hit = hitboxes.find((h) => x >= h.start && x < h.end);
2796
+ if (hit) {
2797
+ state.activePanel = state.activePanel === hit.id ? "grid" : hit.id;
2798
+ setStatus(state.activePanel === "grid" ? "Grid view" : `Panel: ${hit.id.toUpperCase()}`);
2799
+ render();
2734
2800
  }
2735
- onClose() {
2736
- if (this.running) {
2737
- this.status("Connection closed \u2014 reconnecting in 3s");
2738
- this.reconnectTimer = setTimeout(() => {
2739
- if (this.running) this.connect().catch(() => {
2740
- });
2741
- }, 3e3);
2742
- }
2801
+ }
2802
+ function stopLive() {
2803
+ if (state.intervalHandle) {
2804
+ clearInterval(state.intervalHandle);
2805
+ state.intervalHandle = null;
2743
2806
  }
2744
- onError(err) {
2745
- const code = err.code;
2746
- if (code === "ECONNREFUSED") {
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
- }
2807
+ if (state.statusTimer) {
2808
+ clearTimeout(state.statusTimer);
2809
+ state.statusTimer = null;
2755
2810
  }
2756
- onTimeout() {
2757
- this.status("Connection timed out");
2758
- this.disconnect();
2811
+ if (state.logStream) {
2812
+ state.logStream.end();
2813
+ state.logStream = null;
2759
2814
  }
2760
- status(msg) {
2761
- if (this.onStatusCallback) {
2762
- this.onStatusCallback(msg);
2763
- }
2815
+ if (state.p2pServer) {
2816
+ state.p2pServer.stop();
2817
+ state.p2pServer = null;
2818
+ state.p2pServerRunning = false;
2764
2819
  }
2765
- audit(event, detail) {
2766
- if (!this.auditStream) return;
2767
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2768
- const ip = this.options.host;
2769
- this.auditStream.write(`${ts} ${ip} ${event} ${detail}
2770
- `);
2820
+ if (state.keypressHandler) {
2821
+ process.stdin.removeListener("keypress", state.keypressHandler);
2822
+ state.keypressHandler = null;
2771
2823
  }
2772
- };
2773
- function deriveHMACKey2(password) {
2774
- return crypto3.createHash("sha256").update(password).digest("hex").slice(0, 32);
2824
+ process.stdout.removeListener("resize", onResize);
2825
+ state.running = false;
2826
+ state.logging = false;
2827
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
2828
+ process.stdin.pause();
2829
+ if (state.mouseEnabled) {
2830
+ process.stdout.write("\x1B[?1000l");
2831
+ }
2832
+ process.stdout.write("\x1B[?25h");
2833
+ process.stdout.write("\x1B[?1049l");
2834
+ process.stdout.write("\x1B[2J\x1B[H");
2835
+ process.exit(0);
2836
+ }
2837
+
2838
+ // src/splash.ts
2839
+ import chalk8 from "chalk";
2840
+ var MAXV = 36;
2841
+ var CHAR_RAMP = [" ", ".", "`", ",", ":", ";", "+", "*", "?", "%", "#", "@"];
2842
+ var palette = [
2843
+ "#0a0505",
2844
+ "#1a0806",
2845
+ "#2a0b06",
2846
+ "#3f1004",
2847
+ "#591404",
2848
+ "#791804",
2849
+ "#9e1c04",
2850
+ "#c92004",
2851
+ "#d63004",
2852
+ "#e34004",
2853
+ "#f05004",
2854
+ "#fd6004",
2855
+ "#ff7004",
2856
+ "#ff8004",
2857
+ "#ff9004",
2858
+ "#ffa004",
2859
+ "#ffb004",
2860
+ "#ffc004",
2861
+ "#ffd004",
2862
+ "#ffe004",
2863
+ "#fff004",
2864
+ "#fff820",
2865
+ "#ffff40",
2866
+ "#ffff60",
2867
+ "#ffff80",
2868
+ "#ffffa0",
2869
+ "#ffffc0",
2870
+ "#ffffe0",
2871
+ "#ffffff",
2872
+ "#ffffff",
2873
+ "#ffffff",
2874
+ "#ffffff",
2875
+ "#ffffff",
2876
+ "#ffffff",
2877
+ "#ffffff",
2878
+ "#ffffff"
2879
+ ];
2880
+ function getRgb(hex) {
2881
+ const r = parseInt(hex.slice(1, 3), 16);
2882
+ const g = parseInt(hex.slice(3, 5), 16);
2883
+ const b = parseInt(hex.slice(5, 7), 16);
2884
+ return { r, g, b };
2885
+ }
2886
+ var rgbPalette = palette.map(getRgb);
2887
+ function getChar(val) {
2888
+ const idx = Math.min(CHAR_RAMP.length - 1, Math.floor(val / (MAXV + 1) * CHAR_RAMP.length));
2889
+ return CHAR_RAMP[idx];
2890
+ }
2891
+ async function showSplash() {
2892
+ const cols = 60;
2893
+ const rows = 15;
2894
+ let fire = new Uint8Array(cols * rows);
2895
+ process.stdout.write("\x1B[2J");
2896
+ const termRows = process.stdout.rows || 24;
2897
+ const termCols = process.stdout.columns || 80;
2898
+ const startRow = Math.max(0, Math.floor((termRows - rows) / 2));
2899
+ const startCol = Math.max(0, Math.floor((termCols - cols) / 2));
2900
+ return new Promise((resolve) => {
2901
+ const interval = setInterval(() => {
2902
+ for (let x = 0; x < cols; x++) {
2903
+ fire[(rows - 1) * cols + x] = MAXV - Math.floor(Math.random() * 4);
2904
+ }
2905
+ for (let y = rows - 1; y >= 1; y--) {
2906
+ for (let x = 0; x < cols; x++) {
2907
+ const src = y * cols + x;
2908
+ const val = fire[src];
2909
+ if (val === 0) {
2910
+ fire[(y - 1) * cols + x] = 0;
2911
+ continue;
2912
+ }
2913
+ const decay = Math.floor(Math.random() * 3);
2914
+ const drift = Math.floor(Math.random() * 3) - 1;
2915
+ let nx = x + drift;
2916
+ if (nx < 0) nx = 0;
2917
+ else if (nx >= cols) nx = cols - 1;
2918
+ fire[(y - 1) * cols + nx] = Math.max(0, val - decay);
2919
+ }
2920
+ }
2921
+ let output = "";
2922
+ for (let i = 0; i < startRow; i++) output += "\n";
2923
+ for (let y = 0; y < rows; y++) {
2924
+ output += " ".repeat(startCol);
2925
+ for (let x = 0; x < cols; x++) {
2926
+ const v = fire[y * cols + x];
2927
+ const color = rgbPalette[Math.min(v, rgbPalette.length - 1)];
2928
+ output += chalk8.rgb(color.r, color.g, color.b)(getChar(v));
2929
+ }
2930
+ output += "\n";
2931
+ }
2932
+ output += " ".repeat(Math.max(0, startCol + (cols - 45) / 2)) + chalk8.bold.white("\u{1F525} pyre \u2014 system monitor (warming up sensors...)\n");
2933
+ output += " ".repeat(Math.max(0, startCol + (cols - 25) / 2)) + chalk8.dim("[Press Enter to continue]");
2934
+ process.stdout.write(`\x1B[H${output}`);
2935
+ }, 65);
2936
+ const onEnter = () => {
2937
+ process.stdin.removeListener("data", onEnter);
2938
+ clearInterval(interval);
2939
+ process.stdout.write("\x1B[2J\x1B[H");
2940
+ resolve();
2941
+ };
2942
+ process.stdin.setRawMode(true);
2943
+ process.stdin.resume();
2944
+ process.stdin.on("data", onEnter);
2945
+ setTimeout(onEnter, 5e3);
2946
+ });
2775
2947
  }
2776
2948
 
2777
2949
  // src/index.ts