diff --git a/bin/cli/commands/dashboard.mjs b/bin/cli/commands/dashboard.mjs index 0652d3549e..cce5c3d621 100644 --- a/bin/cli/commands/dashboard.mjs +++ b/bin/cli/commands/dashboard.mjs @@ -8,7 +8,17 @@ export function registerDashboard(program) { .description(t("dashboard.description")) .option("--url", t("dashboard.urlOnly")) .option("--port ", "Port the server is running on", "20128") - .action(async (opts) => { + .option("--tui", t("dashboard.tui") || "Open interactive TUI dashboard (terminal UI)") + .action(async (opts, cmd) => { + if (opts.tui) { + const globalOpts = cmd.optsWithGlobals(); + const port = opts.port ? parseInt(String(opts.port), 10) : 20128; + const baseUrl = globalOpts.baseUrl ?? `http://localhost:${port}`; + const apiKey = globalOpts.apiKey ?? null; + const { startInteractiveTui } = await import("../tui/Dashboard.jsx"); + await startInteractiveTui({ port, baseUrl, apiKey }); + return; + } const exitCode = await runDashboardCommand(opts); if (exitCode !== 0) process.exit(exitCode); }); diff --git a/bin/cli/locales/en.json b/bin/cli/locales/en.json index bab7948ad8..c4f3c563f7 100644 --- a/bin/cli/locales/en.json +++ b/bin/cli/locales/en.json @@ -379,7 +379,8 @@ "dashboard": { "description": "Open the OmniRoute dashboard in a browser", "opening": "Opening dashboard at {url}", - "urlOnly": "Print dashboard URL without opening browser" + "urlOnly": "Print dashboard URL without opening browser", + "tui": "Open interactive TUI dashboard (terminal UI, 7 tabs)" }, "models": { "description": "List available models (requires server)", diff --git a/bin/cli/locales/pt-BR.json b/bin/cli/locales/pt-BR.json index 6aef7154f7..3de8a6f7ff 100644 --- a/bin/cli/locales/pt-BR.json +++ b/bin/cli/locales/pt-BR.json @@ -379,7 +379,8 @@ "dashboard": { "description": "Abrir o painel OmniRoute no navegador", "opening": "Abrindo painel em {url}", - "urlOnly": "Exibir URL do painel sem abrir o navegador" + "urlOnly": "Exibir URL do painel sem abrir o navegador", + "tui": "Abrir painel TUI interativo (terminal UI, 7 abas)" }, "models": { "description": "Listar modelos disponíveis (requer servidor)", diff --git a/bin/cli/tui-components/CodeBlock.jsx b/bin/cli/tui-components/CodeBlock.jsx new file mode 100644 index 0000000000..8447ce5c03 --- /dev/null +++ b/bin/cli/tui-components/CodeBlock.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Box, Text } from "ink"; + +export function CodeBlock({ code = "", language }) { + return ( + + {language && ( + + {language} + + )} + {code} + + ); +} diff --git a/bin/cli/tui-components/ConfirmDialog.jsx b/bin/cli/tui-components/ConfirmDialog.jsx new file mode 100644 index 0000000000..2cba868af5 --- /dev/null +++ b/bin/cli/tui-components/ConfirmDialog.jsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; +import { Box, Text, useInput } from "ink"; + +export function ConfirmDialog({ message, onConfirm, onCancel }) { + const [selected, setSelected] = useState(1); // 0=yes, 1=no (default no) + + useInput((input, key) => { + if (key.leftArrow || input === "y") setSelected(0); + if (key.rightArrow || input === "n") setSelected(1); + if (key.return) { + if (selected === 0) onConfirm?.(); + else onCancel?.(); + } + if (key.escape) onCancel?.(); + }); + + return ( + + + {message} + + + + Yes + + + No + + + + ); +} diff --git a/bin/cli/tui-components/DataTable.jsx b/bin/cli/tui-components/DataTable.jsx new file mode 100644 index 0000000000..dc602ca360 --- /dev/null +++ b/bin/cli/tui-components/DataTable.jsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; +import { Box, Text, useInput } from "ink"; +import { theme } from "./theme.jsx"; + +function formatCell(v, col) { + if (v == null) return "-"; + if (col.formatter) return col.formatter(v); + return String(v); +} + +export function DataTable({ rows = [], schema = [], selectable = false, onSelect }) { + const [selectedIdx, setSelectedIdx] = useState(0); + + useInput((input, key) => { + if (!selectable || rows.length === 0) return; + if (key.upArrow) setSelectedIdx((i) => Math.max(0, i - 1)); + if (key.downArrow) setSelectedIdx((i) => Math.min(rows.length - 1, i + 1)); + if (key.return && onSelect) onSelect(rows[selectedIdx]); + }); + + if (rows.length === 0) { + return No data.; + } + + return ( + + + {schema.map((col) => ( + + + {col.header} + + + ))} + + {rows.map((row, idx) => ( + + {schema.map((col) => ( + + {formatCell(row[col.key], col)} + + ))} + + ))} + + ); +} diff --git a/bin/cli/tui-components/HeaderSwr.jsx b/bin/cli/tui-components/HeaderSwr.jsx new file mode 100644 index 0000000000..2306ebc5af --- /dev/null +++ b/bin/cli/tui-components/HeaderSwr.jsx @@ -0,0 +1,25 @@ +import React, { useEffect, useState } from "react"; +import { Text } from "ink"; + +export function HeaderSwr({ fetcher, interval = 5000, render, initial = null }) { + const [data, setData] = useState(initial); + + useEffect(() => { + let cancelled = false; + async function tick() { + try { + const next = await fetcher(); + if (!cancelled) setData(next); + } catch {} + } + tick(); + const id = setInterval(tick, interval); + return () => { + cancelled = true; + clearInterval(id); + }; + }, [fetcher, interval]); + + if (!data) return Loading…; + return render(data); +} diff --git a/bin/cli/tui-components/KeyMaskedDisplay.jsx b/bin/cli/tui-components/KeyMaskedDisplay.jsx new file mode 100644 index 0000000000..d0581882d2 --- /dev/null +++ b/bin/cli/tui-components/KeyMaskedDisplay.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Text } from "ink"; + +export function KeyMaskedDisplay({ apiKey, revealed = false }) { + if (!apiKey) return (none); + const display = revealed + ? apiKey + : apiKey.length <= 8 + ? "***" + : `${apiKey.slice(0, 6)}***${apiKey.slice(-4)}`; + return {display}; +} diff --git a/bin/cli/tui-components/MarkdownView.jsx b/bin/cli/tui-components/MarkdownView.jsx new file mode 100644 index 0000000000..91efe78fe7 --- /dev/null +++ b/bin/cli/tui-components/MarkdownView.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Text } from "ink"; + +export function MarkdownView({ content = "" }) { + // Render markdown as plain text with basic bold/italic stripping. + // For rich rendering, use marked-terminal externally before passing content. + const clean = content + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1") + .replace(/`(.+?)`/g, "$1") + .replace(/^#+\s+/gm, ""); + return {clean}; +} diff --git a/bin/cli/tui-components/MenuSelect.jsx b/bin/cli/tui-components/MenuSelect.jsx new file mode 100644 index 0000000000..d7174aa104 --- /dev/null +++ b/bin/cli/tui-components/MenuSelect.jsx @@ -0,0 +1,38 @@ +import React, { useState } from "react"; +import { Box, Text, useInput } from "ink"; + +export function MenuSelect({ items = [], onSelect, initial = 0 }) { + const [idx, setIdx] = useState(Math.min(initial, Math.max(0, items.length - 1))); + + useInput((input, key) => { + if (items.length === 0) return; + if (key.upArrow) setIdx((i) => (i - 1 + items.length) % items.length); + if (key.downArrow) setIdx((i) => (i + 1) % items.length); + if (key.return && onSelect) onSelect(items[idx]); + const n = parseInt(input, 10); + if (!isNaN(n) && n >= 1 && n <= items.length) { + const newIdx = n - 1; + setIdx(newIdx); + if (onSelect) onSelect(items[newIdx]); + } + }); + + return ( + + {items.map((item, i) => ( + + + {i === idx ? "▶ " : " "} + {item.label} + + {item.hint && ( + + {" "} + {item.hint} + + )} + + ))} + + ); +} diff --git a/bin/cli/tui-components/MultilineInput.jsx b/bin/cli/tui-components/MultilineInput.jsx new file mode 100644 index 0000000000..bafdcf3e64 --- /dev/null +++ b/bin/cli/tui-components/MultilineInput.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Box, Text } from "ink"; +import TextInput from "ink-text-input"; + +export function MultilineInput({ label, value, onChange, placeholder }) { + return ( + + {label && ( + + {label} + + )} + + + + + ); +} diff --git a/bin/cli/tui-components/ProgressBar.jsx b/bin/cli/tui-components/ProgressBar.jsx new file mode 100644 index 0000000000..92bb28112c --- /dev/null +++ b/bin/cli/tui-components/ProgressBar.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Box, Text } from "ink"; + +export function ProgressBar({ value = 0, max = 100, width = 20, color = "green" }) { + const pct = Math.min(1, Math.max(0, value / max)); + const filled = Math.round(pct * width); + const empty = width - filled; + return ( + + {"█".repeat(filled)} + {"░".repeat(empty)} + {Math.round(pct * 100)}% + + ); +} diff --git a/bin/cli/tui-components/Sparkline.jsx b/bin/cli/tui-components/Sparkline.jsx new file mode 100644 index 0000000000..1d500a58b6 --- /dev/null +++ b/bin/cli/tui-components/Sparkline.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Text } from "ink"; + +const BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]; + +export function Sparkline({ data = [], color = "cyan", width = 10 }) { + const slice = data.slice(-width); + if (slice.length === 0) return {"─".repeat(width)}; + const max = Math.max(...slice, 1); + const chars = slice.map((v) => { + const idx = Math.round((v / max) * (BARS.length - 1)); + return BARS[Math.min(Math.max(idx, 0), BARS.length - 1)]; + }); + return {chars.join("")}; +} diff --git a/bin/cli/tui-components/StatusBadge.jsx b/bin/cli/tui-components/StatusBadge.jsx new file mode 100644 index 0000000000..1e7f739776 --- /dev/null +++ b/bin/cli/tui-components/StatusBadge.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Text } from "ink"; + +const STATUS_MAP = { + running: { label: "● running", color: "green" }, + stopped: { label: "● stopped", color: "red" }, + starting: { label: "◌ starting", color: "yellow" }, + error: { label: "✗ error", color: "red" }, + unknown: { label: "? unknown", color: "gray" }, + ok: { label: "✓ ok", color: "green" }, + warn: { label: "⚠ warn", color: "yellow" }, +}; + +export function StatusBadge({ status = "unknown" }) { + const s = STATUS_MAP[status] ?? { label: status, color: "gray" }; + return {s.label}; +} diff --git a/bin/cli/tui-components/TokenCounter.jsx b/bin/cli/tui-components/TokenCounter.jsx new file mode 100644 index 0000000000..25df9d0d14 --- /dev/null +++ b/bin/cli/tui-components/TokenCounter.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Box, Text } from "ink"; + +export function TokenCounter({ tokensIn = 0, tokensOut = 0, costUsd = 0, model }) { + return ( + + + In: + {tokensIn.toLocaleString()} + Out: + {tokensOut.toLocaleString()} + Cost: + ${costUsd.toFixed(4)} + + {model && Model: {model}} + + ); +} diff --git a/bin/cli/tui-components/theme.jsx b/bin/cli/tui-components/theme.jsx new file mode 100644 index 0000000000..ac6b5a45f3 --- /dev/null +++ b/bin/cli/tui-components/theme.jsx @@ -0,0 +1,11 @@ +export const theme = { + primary: "cyan", + secondary: "yellow", + success: "green", + error: "red", + warning: "yellow", + muted: "gray", + header: "cyan", + selected: "blue", + border: "cyan", +}; diff --git a/bin/cli/tui/Dashboard.jsx b/bin/cli/tui/Dashboard.jsx new file mode 100644 index 0000000000..1671350d33 --- /dev/null +++ b/bin/cli/tui/Dashboard.jsx @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import { render, Box, Text, useInput } from "ink"; +import Overview from "./tabs/Overview.jsx"; +import Combos from "./tabs/Combos.jsx"; +import Providers from "./tabs/Providers.jsx"; +import Keys from "./tabs/Keys.jsx"; +import Logs from "./tabs/Logs.jsx"; +import Health from "./tabs/Health.jsx"; +import Cost from "./tabs/Cost.jsx"; + +const TABS = [ + { id: "overview", label: "Overview", Component: Overview }, + { id: "combos", label: "Combos", Component: Combos }, + { id: "providers", label: "Providers", Component: Providers }, + { id: "keys", label: "Keys", Component: Keys }, + { id: "logs", label: "Logs", Component: Logs }, + { id: "health", label: "Health", Component: Health }, + { id: "cost", label: "Cost $", Component: Cost }, +]; + +function DashboardApp({ port, baseUrl, apiKey, onExit }) { + const [active, setActive] = useState(0); + + useInput((input, key) => { + if (input === "q" || (key.ctrl && input === "c")) onExit(); + const n = parseInt(input, 10); + if (n >= 1 && n <= TABS.length) setActive(n - 1); + if (key.tab && !key.shift) setActive((a) => (a + 1) % TABS.length); + if (key.tab && key.shift) setActive((a) => (a - 1 + TABS.length) % TABS.length); + }); + + const ActiveComponent = TABS[active]?.Component; + + return ( + + + + OmniRoute + + | + {TABS.map((tab, i) => ( + + [{i + 1}]{tab.label} + + ))} + + + {ActiveComponent && } + + + [q]uit [Tab] next [1-7] jump [r]efresh [/]filter + + + ); +} + +export async function startInteractiveTui({ port = 20128, baseUrl, apiKey } = {}) { + const resolvedUrl = baseUrl ?? `http://localhost:${port}`; + return new Promise((resolve) => { + const { unmount, waitUntilExit } = render( + unmount()} /> + ); + waitUntilExit().then(resolve).catch(resolve); + }); +} diff --git a/bin/cli/tui/InterfaceMenu.jsx b/bin/cli/tui/InterfaceMenu.jsx new file mode 100644 index 0000000000..853e26688c --- /dev/null +++ b/bin/cli/tui/InterfaceMenu.jsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { render, Box, Text, useInput } from "ink"; +import { MenuSelect } from "../tui-components/MenuSelect.jsx"; + +function InterfaceMenuApp({ version, baseUrl, hasUpdate, latestVersion, onChoice }) { + return ( + + + + ⚡ OmniRoute {version ? `v${version}` : ""} + + {baseUrl} + + {hasUpdate && ( + + + ↑ Update available: v{latestVersion} (run `omniroute update --apply`) + + + )} + + onChoice(item.label)} + /> + + + [↑↓] navigate [Enter] select [1-5] shortcut [q] exit + + + ); +} + +export async function showInterfaceMenu({ version, baseUrl, hasUpdate, latestVersion } = {}) { + return new Promise((resolve) => { + const { unmount } = render( + { + unmount(); + resolve(choice); + }} + /> + ); + }); +} diff --git a/bin/cli/tui/tabs/Combos.jsx b/bin/cli/tui/tabs/Combos.jsx new file mode 100644 index 0000000000..baac39e434 --- /dev/null +++ b/bin/cli/tui/tabs/Combos.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { HeaderSwr } from "../../tui-components/HeaderSwr.jsx"; +import { DataTable } from "../../tui-components/DataTable.jsx"; + +const SCHEMA = [ + { key: "name", header: "Name", width: 20 }, + { key: "strategy", header: "Strategy", width: 14 }, + { key: "targets", header: "Targets", width: 8 }, + { key: "enabled", header: "Enabled", width: 8, formatter: (v) => (v ? "✓" : "✗") }, +]; + +export default function Combos({ baseUrl, apiKey }) { + const fetcher = React.useCallback(async () => { + const res = await fetch(`${baseUrl}/api/combos`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + }); + return res.ok ? res.json() : []; + }, [baseUrl, apiKey]); + + return ( + + { + const rows = Array.isArray(data) ? data : (data?.combos ?? []); + return ( + ({ + name: c.name, + strategy: c.strategy, + targets: c.targets?.length ?? 0, + enabled: c.enabled !== false, + }))} + schema={SCHEMA} + selectable + /> + ); + }} + /> + + [↑↓] select [Enter] details [r] refresh + + + ); +} diff --git a/bin/cli/tui/tabs/Cost.jsx b/bin/cli/tui/tabs/Cost.jsx new file mode 100644 index 0000000000..daa6ec5435 --- /dev/null +++ b/bin/cli/tui/tabs/Cost.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { HeaderSwr } from "../../tui-components/HeaderSwr.jsx"; +import { Sparkline } from "../../tui-components/Sparkline.jsx"; + +export default function Cost({ baseUrl, apiKey }) { + const fetcher = React.useCallback(async () => { + const res = await fetch(`${baseUrl}/api/usage?period=7d&breakdown=provider`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + }); + return res.ok ? res.json() : null; + }, [baseUrl, apiKey]); + + return ( + { + const byProvider = data?.byProvider ?? {}; + const dailyCosts = data?.dailyCosts ?? []; + const totalCost = data?.totalCost ?? 0; + return ( + + + + Total (7d) + ${totalCost.toFixed(4)} + + + Daily Trend + + + + {Object.keys(byProvider).length > 0 && ( + + By Provider + {Object.entries(byProvider).map(([provider, cost]) => ( + + + {provider} + + ${Number(cost).toFixed(4)} + + ))} + + )} + + ); + }} + /> + ); +} diff --git a/bin/cli/tui/tabs/Health.jsx b/bin/cli/tui/tabs/Health.jsx new file mode 100644 index 0000000000..9a4ca2b6c7 --- /dev/null +++ b/bin/cli/tui/tabs/Health.jsx @@ -0,0 +1,53 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { HeaderSwr } from "../../tui-components/HeaderSwr.jsx"; +import { StatusBadge } from "../../tui-components/StatusBadge.jsx"; + +export default function Health({ baseUrl, apiKey }) { + const fetcher = React.useCallback(async () => { + const res = await fetch(`${baseUrl}/api/monitoring/health?detail=true`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + }); + return res.ok ? res.json() : null; + }, [baseUrl, apiKey]); + + return ( + { + const components = data?.components ?? {}; + const alerts = data?.alerts ?? []; + return ( + + + Components + {Object.entries(components).map(([name, status]) => ( + + + {name} + + + + ))} + + {alerts.length > 0 && ( + + + Alerts ({alerts.length}) + + {alerts.map((a, i) => ( + + ⚠ {a.message ?? a} + + ))} + + )} + + ); + }} + /> + ); +} diff --git a/bin/cli/tui/tabs/Keys.jsx b/bin/cli/tui/tabs/Keys.jsx new file mode 100644 index 0000000000..0bf4d7cc21 --- /dev/null +++ b/bin/cli/tui/tabs/Keys.jsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { HeaderSwr } from "../../tui-components/HeaderSwr.jsx"; +import { DataTable } from "../../tui-components/DataTable.jsx"; +import { KeyMaskedDisplay } from "../../tui-components/KeyMaskedDisplay.jsx"; + +const SCHEMA = [ + { key: "label", header: "Label", width: 20 }, + { key: "key", header: "Key", width: 24 }, + { key: "scope", header: "Scope", width: 12 }, + { key: "active", header: "Active", width: 8, formatter: (v) => (v ? "✓" : "✗") }, +]; + +export default function Keys({ baseUrl, apiKey }) { + const fetcher = React.useCallback(async () => { + const res = await fetch(`${baseUrl}/api/v1/registered-keys`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + }); + return res.ok ? res.json() : []; + }, [baseUrl, apiKey]); + + return ( + + { + const rows = Array.isArray(data) ? data : (data?.keys ?? []); + return ( + ({ + label: k.label ?? k.id, + key: k.key ? `${k.key.slice(0, 6)}***${k.key.slice(-4)}` : "***", + scope: k.scope ?? "all", + active: k.active !== false, + }))} + schema={SCHEMA} + selectable + /> + ); + }} + /> + + [↑↓] select [a] add [r] revoke [R] reveal [c] copy + + + ); +} diff --git a/bin/cli/tui/tabs/Logs.jsx b/bin/cli/tui/tabs/Logs.jsx new file mode 100644 index 0000000000..fa105fe8c8 --- /dev/null +++ b/bin/cli/tui/tabs/Logs.jsx @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from "react"; +import { Box, Text, useInput } from "ink"; + +const MAX_LINES = 40; + +export default function Logs({ baseUrl, apiKey }) { + const [lines, setLines] = useState([]); + const [paused, setPaused] = useState(false); + + useInput((input, key) => { + if (input === "p") setPaused((v) => !v); + if (input === "c") setLines([]); + }); + + useEffect(() => { + if (paused) return; + let cancelled = false; + const headers = { Accept: "text/event-stream" }; + if (apiKey) headers.Authorization = `Bearer ${apiKey}`; + + fetch(`${baseUrl}/api/v1/logs/stream?limit=50`, { headers }) + .then(async (res) => { + if (!res.ok || !res.body) return; + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + while (!cancelled) { + const { done, value } = await reader.read(); + if (done) break; + const text = decoder.decode(value); + for (const line of text.split("\n")) { + if (line.startsWith("data:")) { + const payload = line.slice(5).trim(); + if (payload && !cancelled) { + setLines((prev) => [...prev.slice(-(MAX_LINES - 1)), payload]); + } + } + } + } + }) + .catch(() => {}); + + return () => { + cancelled = true; + }; + }, [baseUrl, apiKey, paused]); + + return ( + + + {lines.length === 0 && Waiting for log events…} + {lines.map((line, i) => ( + {line} + ))} + + + {paused ? "[PAUSED]" : "[LIVE]"} [p] pause/resume [c] clear + + + ); +} diff --git a/bin/cli/tui/tabs/Overview.jsx b/bin/cli/tui/tabs/Overview.jsx new file mode 100644 index 0000000000..bdcbb32b62 --- /dev/null +++ b/bin/cli/tui/tabs/Overview.jsx @@ -0,0 +1,80 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { HeaderSwr } from "../../tui-components/HeaderSwr.jsx"; +import { Sparkline } from "../../tui-components/Sparkline.jsx"; +import { StatusBadge } from "../../tui-components/StatusBadge.jsx"; + +function formatUptime(seconds) { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return `${d}d ${h}h`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} + +function OverviewContent({ data }) { + const reqs = data?.requests24h ?? 0; + const cost = (data?.cost24h ?? 0).toFixed(4); + const uptimeSecs = data?.uptimeSeconds ?? 0; + const sparkData = data?.requestsSparkline ?? []; + + return ( + + + + Server + + + + Uptime + {formatUptime(uptimeSecs)} + + + Requests/24h + + {reqs.toLocaleString()} + + + + + Cost/24h + ${cost} + + + {data?.recentActivity?.length > 0 && ( + + + Recent Activity + + {data.recentActivity.slice(0, 5).map((r, i) => ( + + {r.time} + {r.status < 400 ? "✓" : "✗"} + {r.path} + {r.model} + {r.duration}ms + + ))} + + )} + + ); +} + +export default function Overview({ baseUrl, apiKey }) { + const fetcher = React.useCallback(async () => { + const res = await fetch(`${baseUrl}/api/monitoring/health`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + }); + return res.ok ? res.json() : null; + }, [baseUrl, apiKey]); + + return ( + } + /> + ); +} diff --git a/bin/cli/tui/tabs/Providers.jsx b/bin/cli/tui/tabs/Providers.jsx new file mode 100644 index 0000000000..a314341d90 --- /dev/null +++ b/bin/cli/tui/tabs/Providers.jsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Box, Text } from "ink"; +import { HeaderSwr } from "../../tui-components/HeaderSwr.jsx"; +import { DataTable } from "../../tui-components/DataTable.jsx"; +import { StatusBadge } from "../../tui-components/StatusBadge.jsx"; + +const SCHEMA = [ + { key: "name", header: "Provider", width: 16 }, + { key: "status", header: "Status", width: 12, formatter: (v) => v }, + { key: "accounts", header: "Accounts", width: 10 }, + { key: "models", header: "Models", width: 8 }, +]; + +export default function Providers({ baseUrl, apiKey }) { + const fetcher = React.useCallback(async () => { + const res = await fetch(`${baseUrl}/api/providers`, { + headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, + }); + return res.ok ? res.json() : []; + }, [baseUrl, apiKey]); + + return ( + + { + const rows = Array.isArray(data) ? data : (data?.providers ?? []); + return ( + ({ + name: p.name ?? p.id, + status: p.status ?? "unknown", + accounts: p.accountCount ?? 0, + models: p.modelCount ?? 0, + }))} + schema={SCHEMA} + selectable + /> + ); + }} + /> + + [↑↓] select [Enter] details [t] test [r] refresh + + + ); +} diff --git a/package-lock.json b/package-lock.json index 6292690234..dff1e5ad7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,9 @@ "gray-matter": "^4.0.3", "http-proxy-middleware": "^4.0.0", "https-proxy-agent": "^9.0.0", + "ink": "^5.2.1", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", "ioredis": "^5.10.1", "isomorphic-dompurify": "^3.12.0", "jose": "^6.2.3", @@ -39,6 +42,7 @@ "lowdb": "^7.0.1", "lucide-react": "^1.14.0", "marked": "^18.0.3", + "marked-terminal": "^7.3.0", "mermaid": "^11.14.0", "monaco-editor": "^0.55.1", "next": "^16.2.6", @@ -53,6 +57,7 @@ "react-dom": "19.2.6", "react-is": "^19.2.6", "react-markdown": "^10.1.0", + "react-reconciler": "^0.31.0", "recharts": "^3.8.1", "selfsigned": "^5.5.0", "tls-client-node": "^0.1.13", @@ -114,6 +119,43 @@ "dev": true, "license": "MIT" }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz", + "integrity": "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -3519,6 +3561,18 @@ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -5414,7 +5468,6 @@ "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", - "dev": true, "license": "MIT", "dependencies": { "environment": "^1.0.0" @@ -5439,7 +5492,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5471,6 +5523,12 @@ "react": ">=18" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5713,6 +5771,18 @@ "when-exit": "^2.1.4" } }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -6266,7 +6336,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -6283,7 +6352,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6292,6 +6360,15 @@ "node": ">=8" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -6365,6 +6442,129 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-highlight/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/cli-highlight/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/cli-spinners": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", @@ -6552,11 +6752,22 @@ "node": ">=0.10.0" } }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6569,7 +6780,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -6703,6 +6913,15 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -7731,6 +7950,12 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -7789,7 +8014,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -8043,7 +8267,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9161,7 +9384,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -9454,7 +9676,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9591,6 +9812,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -9834,6 +10064,318 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ink": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", + "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.22.0", + "indent-string": "^5.0.0", + "is-in-ci": "^1.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-spinner": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ink-spinner/-/ink-spinner-5.0.0.tgz", + "integrity": "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==", + "license": "MIT", + "dependencies": { + "cli-spinners": "^2.7.0" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "ink": ">=4.0.0", + "react": ">=18.0.0" + } + }, + "node_modules/ink-spinner/node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/ink-text-input/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/ink/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/ink/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/ink/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/ink/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/ink/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -10172,7 +10714,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, "license": "MIT", "dependencies": { "get-east-asian-width": "^1.3.1" @@ -11501,7 +12042,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -11595,6 +12135,51 @@ "node": ">= 20" } }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12327,6 +12912,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -12439,6 +13033,17 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -12625,6 +13230,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -13079,6 +13699,21 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13088,6 +13723,15 @@ "node": ">= 0.8" } }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -13749,6 +14393,27 @@ "react": ">=18" } }, + "node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-reconciler/node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -13999,7 +14664,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14707,6 +15371,18 @@ "simple-concat": "^1.0.0" } }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -14811,6 +15487,27 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -15196,6 +15893,34 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -15317,6 +16042,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/thread-stream": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", @@ -15802,6 +16548,15 @@ "dev": true, "license": "MIT" }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -16629,6 +17384,27 @@ "win32" ] }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", @@ -16695,7 +17471,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -16833,6 +17608,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 672ef0f404..2c8cae82ae 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,9 @@ "gray-matter": "^4.0.3", "http-proxy-middleware": "^4.0.0", "https-proxy-agent": "^9.0.0", + "ink": "^5.2.1", + "ink-spinner": "^5.0.0", + "ink-text-input": "^6.0.0", "ioredis": "^5.10.1", "isomorphic-dompurify": "^3.12.0", "jose": "^6.2.3", @@ -152,6 +155,7 @@ "lowdb": "^7.0.1", "lucide-react": "^1.14.0", "marked": "^18.0.3", + "marked-terminal": "^7.3.0", "mermaid": "^11.14.0", "monaco-editor": "^0.55.1", "next": "^16.2.6", @@ -166,6 +170,7 @@ "react-dom": "19.2.6", "react-is": "^19.2.6", "react-markdown": "^10.1.0", + "react-reconciler": "^0.31.0", "recharts": "^3.8.1", "selfsigned": "^5.5.0", "tls-client-node": "^0.1.13", diff --git a/tests/unit/cli-tui.test.ts b/tests/unit/cli-tui.test.ts new file mode 100644 index 0000000000..43818fa183 --- /dev/null +++ b/tests/unit/cli-tui.test.ts @@ -0,0 +1,81 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = join(__dirname, "..", ".."); +const TUI_COMPONENTS = join(ROOT, "bin", "cli", "tui-components"); +const TUI = join(ROOT, "bin", "cli", "tui"); + +function hasExport(file: string, name: string): boolean { + const src = readFileSync(file, "utf8"); + return ( + src.includes(`export function ${name}`) || + src.includes(`export async function ${name}`) || + src.includes(`export { ${name}`) + ); +} + +const COMPONENTS = [ + { file: "DataTable.jsx", export: "DataTable" }, + { file: "MenuSelect.jsx", export: "MenuSelect" }, + { file: "ProgressBar.jsx", export: "ProgressBar" }, + { file: "StatusBadge.jsx", export: "StatusBadge" }, + { file: "TokenCounter.jsx", export: "TokenCounter" }, + { file: "KeyMaskedDisplay.jsx", export: "KeyMaskedDisplay" }, + { file: "Sparkline.jsx", export: "Sparkline" }, + { file: "HeaderSwr.jsx", export: "HeaderSwr" }, + { file: "ConfirmDialog.jsx", export: "ConfirmDialog" }, + { file: "MultilineInput.jsx", export: "MultilineInput" }, + { file: "MarkdownView.jsx", export: "MarkdownView" }, + { file: "CodeBlock.jsx", export: "CodeBlock" }, +]; + +for (const { file, export: exp } of COMPONENTS) { + test(`tui-components/${file} existe e exporta ${exp}`, () => { + const path = join(TUI_COMPONENTS, file); + assert.ok(existsSync(path), `${file} deve existir`); + assert.ok(hasExport(path, exp), `${file} deve exportar ${exp}`); + }); +} + +test("tui-components/theme.jsx exporta objeto theme", async () => { + const { theme } = await import("../../bin/cli/tui-components/theme.jsx"); + assert.ok(theme && typeof theme === "object"); + assert.ok(typeof theme.primary === "string"); + assert.ok(typeof theme.success === "string"); + assert.ok(typeof theme.error === "string"); +}); + +test("tui/Dashboard.jsx existe e exporta startInteractiveTui", () => { + const path = join(TUI, "Dashboard.jsx"); + assert.ok(existsSync(path), "Dashboard.jsx deve existir"); + assert.ok(hasExport(path, "startInteractiveTui"), "deve exportar startInteractiveTui"); +}); + +test("tui/InterfaceMenu.jsx existe e exporta showInterfaceMenu", () => { + const path = join(TUI, "InterfaceMenu.jsx"); + assert.ok(existsSync(path), "InterfaceMenu.jsx deve existir"); + assert.ok(hasExport(path, "showInterfaceMenu"), "deve exportar showInterfaceMenu"); +}); + +test("tui/tabs/ tem as 7 tabs esperadas", () => { + const tabs = ["Overview", "Combos", "Providers", "Keys", "Logs", "Health", "Cost"]; + for (const tab of tabs) { + const path = join(TUI, "tabs", `${tab}.jsx`); + assert.ok(existsSync(path), `tabs/${tab}.jsx deve existir`); + } +}); + +test("commands/dashboard.mjs registra --tui flag", async () => { + const { registerDashboard } = await import("../../bin/cli/commands/dashboard.mjs"); + const { Command } = await import("commander"); + const prog = new Command().exitOverride(); + registerDashboard(prog); + const dashCmd = prog.commands.find((c) => c.name() === "dashboard"); + assert.ok(dashCmd, "dashboard command deve existir"); + const tuiOpt = dashCmd?.options.find((o) => o.long === "--tui"); + assert.ok(tuiOpt, "--tui option deve estar registrada"); +});