diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..42becf3 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Mass reformatting - introducing CI Agent (Issue #14) +433cd2b661a88b143fae6e215a487280a5dedf05 + +189ff3dffca02d8824919060b24ef0a084c609af +a91de65471c7ad4145a5b509b06a1d08ca1dfd6f +821770ad1742a746b373d2bd0cf081f2eaebedd9 diff --git a/.github/workflows/agent_ci.yml b/.github/workflows/agent_ci.yml new file mode 100644 index 0000000..0cdde55 --- /dev/null +++ b/.github/workflows/agent_ci.yml @@ -0,0 +1,66 @@ +name: Agent CI + +on: + push: + branches: [ "main", "devel" ] + paths: + - 'sources/agent/**' + - '.github/workflows/agent_ci.yml' + pull_request: + branches: [ "main", "devel" ] + paths: + - 'sources/agent/**' + - '.github/workflows/agent_ci.yml' + +jobs: + build-and-test: + name: Build & Format Check + runs-on: ubuntu-latest + + env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + + steps: + - uses: actions/checkout@v4 + + - name: Setup C++ tools + uses: aminya/setup-cpp@v1 + with: + clang-format: true + cppcheck: true + + - name: Cache vcpkg + uses: actions/cache@v4 + with: + path: | + ~/.cache/vcpkg + sources/agent/build/vcpkg_installed + key: vcpkg-${{ runner.os }}-${{ hashFiles('sources/agent/vcpkg.json') }} + + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgJsonGlob: 'sources/agent/vcpkg.json' + + - name: Check Code Formatting + run: | + find sources/agent/src -name "*.cc" -o -name "*.h" | xargs clang-format --dry-run --Werror -style=Google + + - name: CMake Configure + working-directory: sources/agent + run: cmake --preset default -DCMAKE_BUILD_TYPE=Release + + - name: Build + working-directory: sources/agent + run: cmake --build --preset default -j$(nproc) + + - name: Analyze Logic + run: | + cppcheck --enable=warning,performance,portability \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + --inline-suppr \ + --force \ + -i sources/agent/build \ + -i sources/agent/gen \ + sources/agent diff --git a/.github/workflows/dashboard_ci.yml b/.github/workflows/dashboard_ci.yml new file mode 100644 index 0000000..d820c45 --- /dev/null +++ b/.github/workflows/dashboard_ci.yml @@ -0,0 +1,78 @@ +name: Dashboard CI + +on: + push: + branches: [ "main", "devel" ] + paths: + - 'sources/dashboard/**' + - '.github/workflows/dashboard_ci.yml' + pull_request: + branches: [ "main", "devel" ] + paths: + - 'sources/dashboard/**' + - '.github/workflows/dashboard_ci.yml' + +jobs: + check-and-build: + name: Lint, Format & Build + runs-on: ubuntu-latest + + defaults: + run: + working-directory: sources/dashboard + + steps: + - uses: actions/checkout@v6 + + - uses: voidzero-dev/setup-vp@v1 + with: + node-version: '24' + working-directory: sources/dashboard + cache: true + run-install: true + + - name: Format, lint & typecheck + run: vp check + + - name: Build + run: vp run build + env: + GITHUB_PAGES: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'true' || '' }} + + - name: Upload build artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v7 + with: + name: dashboard-dist + path: sources/dashboard/dist + retention-days: 7 + + deploy: + name: Deploy to GitHub Pages + needs: check-and-build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + concurrency: + group: pages + cancel-in-progress: false + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/download-artifact@v8 + with: + name: dashboard-dist + path: dist + + - uses: actions/configure-pages@v6 + + - uses: actions/upload-pages-artifact@v5 + with: + path: dist + + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/server_ci.yml b/.github/workflows/server_ci.yml new file mode 100644 index 0000000..e9ff8ed --- /dev/null +++ b/.github/workflows/server_ci.yml @@ -0,0 +1,63 @@ +name: Server CI + +on: + push: + branches: [ "main", "devel" ] + paths: + - 'sources/server/**' + - '.github/workflows/server_ci.yml' + pull_request: + branches: [ "main", "devel" ] + paths: + - 'sources/server/**' + - '.github/workflows/server_ci.yml' + +jobs: + build-and-test: + name: Server Lint, Build & Test + runs-on: ubuntu-latest + + defaults: + run: + working-directory: sources/server + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: sources/server/go.mod + cache: true + cache-dependency-path: sources/server/go.sum + + - name: Install protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Go protoc plugins + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + + - name: Generate protobuf + run: make proto + + - name: Verify formatting + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "The following files are not gofmt-formatted:" + echo "$unformatted" + exit 1 + fi + + - name: Go vet + run: go vet ./... + + - name: Build + run: go build ./... + + - name: Test + run: go test -v ./... diff --git a/.gitignore b/.gitignore index c90e3cd..4e757b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ +# --- Shared --- .vscode/ -build/ -cmake-build-*/ +*.code-workspace +sources/**/logs + +# --- Agent --- +.cache/ +compile_commands.json +sources/agent/agent.conf +.clang-format +*.uuid +out/ + +# --- Server --- +sources/server/pb/ +sources/server/server diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 0000000..795880c --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,17 @@ +[[language]] +name = "c" +scope = "source.c" +file-types = [] + +[[language]] +name = "cpp" +scope = "source.cpp" +file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H", "cu", "cuh"] +language-servers = ["clangd"] + +[language-server.clangd] +command = "clangd" +args = [ + "--header-insertion=never", + "--compile-commands-dir=build" +] diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 6800ca1..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,44 +0,0 @@ -cmake_minimum_required(VERSION 3.16) - -project(volta LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -find_package(protobuf CONFIG REQUIRED) -find_package(gRPC CONFIG REQUIRED) - -set(PROTO_DIR ${CMAKE_SOURCE_DIR}/libs/proto) -set(PROTO_FILE ${PROTO_DIR}/volta.proto) - -get_target_property(GRPC_CPP_PLUGIN gRPC::grpc_cpp_plugin LOCATION) - -set(PROTO_SRC "${CMAKE_CURRENT_BINARY_DIR}/volta.pb.cc") -set(PROTO_HDR "${CMAKE_CURRENT_BINARY_DIR}/volta.pb.h") -set(GRPC_SRC "${CMAKE_CURRENT_BINARY_DIR}/volta.grpc.pb.cc") -set(GRPC_HDR "${CMAKE_CURRENT_BINARY_DIR}/volta.grpc.pb.h") - -add_custom_command( - OUTPUT "${PROTO_SRC}" "${PROTO_HDR}" "${GRPC_SRC}" "${GRPC_HDR}" - - COMMAND protobuf::protoc - - ARGS --cpp_out="${CMAKE_CURRENT_BINARY_DIR}" - --grpc_out="${CMAKE_CURRENT_BINARY_DIR}" - --plugin=protoc-gen-grpc="${GRPC_CPP_PLUGIN}" - -I "${PROTO_DIR}" - "${PROTO_FILE}" - - DEPENDS "${PROTO_FILE}" gRPC::grpc_cpp_plugin - COMMENT "Generating C++ code from volta.proto..." -) - -add_library(volta_proto ${PROTO_SRC} ${PROTO_HDR} ${GRPC_SRC} ${GRPC_HDR}) - -target_include_directories(volta_proto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) - -target_link_libraries(volta_proto PRIVATE protobuf::libprotobuf gRPC::grpc++) - -add_subdirectory(source/agent) -#add_subdirectory(source/server) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..365bfc9 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +hooks: + chmod +x scripts/hooks/pre-commit + cd scripts/hooks && ln -sfr ./pre-commit ../../.git/hooks/pre-commit diff --git a/README.md b/README.md index 60188aa..3e5d588 100644 --- a/README.md +++ b/README.md @@ -1 +1,63 @@ -# volta \ No newline at end of file +# Volta + +Volta is a lightweight, energy-aware monitoring platform for tracking system and hardware metrics in real time. + +It follows a classic **Agent → Server → Dashboard** model: + +- One or more **Agents** (C++) run on monitored machines, sample low-level hardware/system metrics and stream them to a single configured server over **gRPC**. +- The **Server** (Go) aggregates metrics from all connected agents and exposes them to clients over **WebSockets**. +- The **Dashboard** (React) visualises live and historical metrics served by the Server. +For the full architecture, deployment models (local vs. remote) and the metric catalogue, see [docs/pl/ARCHITECTURE.md](docs/pl/ARCHITECTURE.md) and [docs/pl/METRICS.md](docs/pl/METRICS.md). + +## Repository Structure + +| Path | Component | Stack | Description | +|---|---|---|---| +| [`sources/agent`](sources/agent) | Agent | C++20, CMake, vcpkg | Collects CPU/GPU power and standard system metrics; streams them to the server. | +| [`sources/server`](sources/server) | Server | Go | Central aggregation point; gRPC ingest, REST + WebSocket API for the dashboard. | +| [`sources/dashboard`](sources/dashboard) | Dashboard | TypeScript, React, Vite | Web UI for visualising live and historical agent metrics. | +| [`sources/proto`](sources/proto) | Proto | Protocol Buffers | Shared gRPC contract between Agent and Server. | +| [`docs`](docs) | Docs | Markdown | Architecture, metrics and other supporting documentation. | + +## Maintainers + +- **Agent** — @FW-Nagorko +- **Server** — @kox13 +- **Dashboard** — @patryk-przybysz + +## Development Setup + +### 1. Initial Requirements + +- **Git** +- **Make** +- Component-specific toolchains — see [`sources/agent/README.md`](sources/agent/README.md), [`sources/server/README.md`](sources/server/README.md) and [`sources/dashboard/README.md`](sources/dashboard/README.md) + +### 2. Clone the Repository + +```bash +git clone https://github.com/monvit/volta.git +cd volta +``` + +### 3. Run a Component + +Follow the dedicated setup steps in respective READMEs. + +- [Agent](/sources/agent/README.md) +- [Server](/sources/server/README.md) +- [Dashboard](/sources/dashboard/README.md) + +### 4. Git Hooks (Optional) + +```bash +make hooks +``` + +## Continuous Integration + +Each component is linted, built and tested independently and only on relevant changes — see [`.github/workflows`](.github/workflows) (`agent_ci.yml`, `server_ci.yml`, `dashboard_ci.yml`). + +## License + +Volta is licensed under the [Apache License 2.0](LICENSE). diff --git a/libs/proto/volta.proto b/libs/proto/volta.proto deleted file mode 100644 index 1eee8fb..0000000 --- a/libs/proto/volta.proto +++ /dev/null @@ -1,22 +0,0 @@ -syntax = "proto3"; - -import "google/protobuf/timestamp.proto"; - -package volta.v1; - -service VoltaCollector { - rpc CollectMetrics(stream MetricBatch) returns (CollectResponse); -} - -message MetricBatch { - google.protobuf.Timestamp timestamp = 1; - - string hostname = 2; - - map metrics = 3; -} - -message CollectResponse { - bool success = 1; - string message = 2; -} diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100755 index 0000000..31e6b14 --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,35 @@ +#!/bin/bash + +REPO_ROOT=$(git rev-parse --show-toplevel) +BUILD_DIR="$REPO_ROOT/sources/agent/build" + +STAGED_CPP=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^sources/agent/.*\.(cc|h)$') +STAGED_GO_SERVER=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^sources/server/.*\.go$') + +if [ -z "$STAGED_CPP" ] && [ -z "$STAGED_GO_SERVER" ]; then + exit 0 +fi + +if [ -n "$STAGED_CPP" ]; then + if [ ! -d "$BUILD_DIR" ]; then + echo "Build directory not found: $BUILD_DIR" + echo "Run cmake --preset debug in sources/agent first." + exit 1 + fi + + cmake --build "$BUILD_DIR" --target check-format + if [ $? -ne 0 ]; then + echo "Formatting errors found. Run 'cmake --build $BUILD_DIR --target format' to fix." + exit 1 + fi +fi + +if [ -n "$STAGED_GO_SERVER" ]; then + UNFORMATTED=$(gofmt -l $STAGED_GO_SERVER) + if [ -n "$UNFORMATTED" ]; then + echo "Go formatting errors found in server files:" + echo "$UNFORMATTED" + echo "Run 'gofmt -w ' to fix." + exit 1 + fi +fi \ No newline at end of file diff --git a/source/agent/CMakeLists.txt b/source/agent/CMakeLists.txt deleted file mode 100644 index 65ea219..0000000 --- a/source/agent/CMakeLists.txt +++ /dev/null @@ -1,43 +0,0 @@ -set(AGENT_NAME volta_agent) -add_executable(${AGENT_NAME}) - -file(GLOB_RECURSE AGENT_SOURCES "src/*.cc") -target_sources(${AGENT_NAME} PRIVATE ${AGENT_SOURCES}) - -target_include_directories(${AGENT_NAME} PRIVATE src) - -target_link_libraries(${AGENT_NAME} PRIVATE volta_proto) - -find_package(Threads REQUIRED) -find_package(protobuf REQUIRED) -find_package(gRPC REQUIRED) - -target_link_libraries(${AGENT_NAME} PRIVATE Threads::Threads) -target_link_libraries(${AGENT_NAME} PRIVATE gRPC::grpc++ protobuf::libprotobuf) - -find_path(NVML_INCLUDE_DIR nvml.h - PATHS ${CMAKE_SOURCE_DIR}/libs/include/nvidia - - /usr/include - /usr/local/include - /usr/include/nvidia - /usr/local/cuda/include - PATH_SUFFIXES nvidia/current gdk -) - -find_library(NVML_LIBRARY NAMES nvidia-ml nvml - PATHS /usr/lib /usr/lib64 /usr/local/lib /usr/lib/x86_64-linux-gnu - PATH_SUFFIXES nvidia/current -) - -if(NVML_INCLUDE_DIR AND NVML_LIBRARY) - message(STATUS "Found NVML: ${NVML_LIBRARY}") - target_include_directories(${AGENT_NAME} PRIVATE ${NVML_INCLUDE_DIR}) - target_link_libraries(${AGENT_NAME} PRIVATE ${NVML_LIBRARY}) - target_compile_definitions(${AGENT_NAME} PRIVATE HAVE_NVML) -else() - message(WARNING "NVML not found - the agent will be built without NVIDIA support.") -endif() - -# 5. TODO: find rocm_smi and oneapi level_zero -# 6. TODO: find PMU (libpfm) diff --git a/source/agent/README.md b/source/agent/README.md deleted file mode 100644 index 9d84bd6..0000000 --- a/source/agent/README.md +++ /dev/null @@ -1 +0,0 @@ -## Agent diff --git a/source/agent/src/collectors/collector.h b/source/agent/src/collectors/collector.h deleted file mode 100644 index 7b692a9..0000000 --- a/source/agent/src/collectors/collector.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef VOLTA_AGENT_SRC_COLLECTORS_COLLECTOR_H_ -#define VOLTA_AGENT_SRC_COLLECTORS_COLLECTOR_H_ - -#include - -#include "metric.h" - -namespace volta { -namespace agent { -namespace collectors { - -class Collector { -public: - virtual ~Collector() = default; - - virtual std::vector Collect() = 0; - - virtual bool Init() { return true; } -}; - -} // namespace collectors -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_SRC_COLLECTORS_COLLECTOR_H_ diff --git a/source/agent/src/collectors/nvml_collector.cc b/source/agent/src/collectors/nvml_collector.cc deleted file mode 100644 index 15a2912..0000000 --- a/source/agent/src/collectors/nvml_collector.cc +++ /dev/null @@ -1,108 +0,0 @@ -#include "collectors/nvml_collector.h" - -#include -#include - -namespace volta { -namespace agent { -namespace collectors { - -NvmlCollector::NvmlCollector() { - // Initialization moved to Init() -} - -NvmlCollector::~NvmlCollector() { - if (initialized_) { - nvmlShutdown(); - } -} - -bool NvmlCollector::Init() { - nvmlReturn_t result = nvmlInit(); - if (result != NVML_SUCCESS) { - std::cerr << "Failed to initialize NVML: " << nvmlErrorString(result) << std::endl; - return false; - } - - result = nvmlDeviceGetHandleByIndex(0, &device_handle_); - if (result != NVML_SUCCESS) { - std::cerr << "Failed to get device handle: " << nvmlErrorString(result) << std::endl; - return false; - } - - initialized_ = true; - return true; -} - -std::vector NvmlCollector::Collect() { - if (!initialized_) return {}; - - std::vector metrics; - unsigned int power_mw = 0; - auto now = std::chrono::system_clock::now().time_since_epoch().count(); - - nvmlReturn_t result = nvmlDeviceGetPowerUsage(device_handle_, &power_mw); - - if (result == NVML_SUCCESS) { - metrics.push_back({ - "gpu_0_power_watts", - static_cast(power_mw) / 1000.0, // mW -> W - now - }); - } - - unsigned int temp_c = 0; - result = nvmlDeviceGetTemperature(device_handle_, NVML_TEMPERATURE_GPU, &temp_c); - if (result == NVML_SUCCESS) { - metrics.push_back({ - "gpu_0_temp_celsius", - static_cast(temp_c), - now - }); - } - - nvmlUtilization_t utilization; - result = nvmlDeviceGetUtilizationRates(device_handle_, &utilization); - - if (result == NVML_SUCCESS) { - metrics.push_back({ - "gpu_0_utilization_percent", - static_cast(utilization.gpu), - now - }); - - metrics.push_back({ - "gpu_0_memory_activity_percent", - static_cast(utilization.memory), - now - }); - } - - nvmlMemory_t memory; - result = nvmlDeviceGetMemoryInfo(device_handle_, &memory); - - if (result == NVML_SUCCESS) { - metrics.push_back({ - "gpu_0_memory_total_bytes", - static_cast(memory.total), - now - }); - metrics.push_back({ - "gpu_0_memory_used_bytes", - static_cast(memory.used), - now - }); - double used_percent = (double)memory.used / memory.total * 100.0; - metrics.push_back({ - "gpu_0_memory_used_percent", - used_percent, - now - }); -} - - return metrics; -} - -} // namespace collectors -} // namespace agent -} // namespace volta diff --git a/source/agent/src/collectors/nvml_collector.h b/source/agent/src/collectors/nvml_collector.h deleted file mode 100644 index a732675..0000000 --- a/source/agent/src/collectors/nvml_collector.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef VOLTA_AGENT_SRC_COLLECTORS_NVML_COLLECTOR_H_ -#define VOLTA_AGENT_SRC_COLLECTORS_NVML_COLLECTOR_H_ - -#include "collectors/collector.h" -#include - -namespace volta { -namespace agent { -namespace collectors { - -class NvmlCollector : public Collector { -public: - NvmlCollector(); - ~NvmlCollector() override; - - bool Init() override; - std::vector Collect() override; - -private: - nvmlDevice_t device_handle_; - bool initialized_ = false; -}; - -} // namespace collectors -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_SRC_COLLECTORS_NVML_COLLECTOR_H_ diff --git a/source/agent/src/collectors/proc_stat_collector.cc b/source/agent/src/collectors/proc_stat_collector.cc deleted file mode 100644 index daedbbc..0000000 --- a/source/agent/src/collectors/proc_stat_collector.cc +++ /dev/null @@ -1,53 +0,0 @@ -#include "collectors/proc_stat_collector.h" - -#include -#include - -namespace volta { -namespace agent { -namespace collectors { - -std::vector ProcStatCollector::Collect() { - uint64_t current_total = 0; - uint64_t current_idle = 0; - - ReadCpuStats(current_total, current_idle); - - uint64_t diff_total = current_total - prev_total_; - uint64_t diff_idle = current_idle - prev_idle_; - - double usage_percent = 0.0; - if (diff_total > 0) usage_percent = (double)(diff_total - diff_idle) / diff_total * 100.0; - - prev_total_ = current_total; - prev_idle_ = current_idle; - - Metric m; - m.name = "cpu_usage_total_percent"; - m.value = usage_percent; - m.timestamp = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); - - return {m}; -} - -void ProcStatCollector::ReadCpuStats(uint64_t& total, uint64_t& idle) { - std::ifstream file("/proc/stat"); - std::string line; - - if (std::getline(file, line)) { - std::istringstream iss(line); - std::string cpu_label; - uint64_t user, nice, system, cur_idle, iowait, irq, softirq, steal; - - iss >> cpu_label >> user >> nice >> system >> cur_idle >> iowait >> irq >> softirq >> steal; - - idle = cur_idle + iowait; - total = user + nice + system + idle + irq + softirq + steal; - } -} - - -} // namespace collectors -} // namespace agent -} // namespace volta \ No newline at end of file diff --git a/source/agent/src/collectors/proc_stat_collector.h b/source/agent/src/collectors/proc_stat_collector.h deleted file mode 100644 index 9253ab7..0000000 --- a/source/agent/src/collectors/proc_stat_collector.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef VOLTA_AGENT_SRC_COLLECTORS_PROC_STAT_COLLECTOR_H_ -#define VOLTA_AGENT_SRC_COLLECTORS_PROC_STAT_COLLECTOR_H_ - -#include - -#include "collectors/collector.h" - -namespace volta { -namespace agent { -namespace collectors { - -class ProcStatCollector : public Collector { -public: - std::vector Collect() override; - -private: - void ReadCpuStats(uint64_t& idle_time, uint64_t& total_time); - - uint64_t prev_total_ = 0; - uint64_t prev_idle_ = 0; -}; - -} // namespace collectors -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_SRC_COLLECTORS_PROC_STAT_COLLECTOR_H_ diff --git a/source/agent/src/collectors/ram_collector.cc b/source/agent/src/collectors/ram_collector.cc deleted file mode 100644 index 05f0060..0000000 --- a/source/agent/src/collectors/ram_collector.cc +++ /dev/null @@ -1,48 +0,0 @@ -#include "collectors/ram_collector.h" - -#include -#include -#include - -namespace volta { -namespace agent { -namespace collectors { - -std::vector RamCollector::Collect() { - uint64_t used = 0; - uint64_t total = 0; - - ReadStats(used, total); - - auto now = std::chrono::system_clock::now().time_since_epoch().count(); - - return { - {"ram_total_bytes", (double) total, now}, - {"ram_used_bytes", (double) used, now}, - {"ram_used_percent", (double) used / total * 100.0, now} - }; -} - -void RamCollector::ReadStats(uint64_t& used, uint64_t& total) { - std::ifstream file("/proc/meminfo"); - std::string line, key; - uint64_t value; - std::string unit; // "kB" - - uint64_t available = 0; - - while (std::getline(file, line)) { - std::istringstream iss(line); - iss >> key >> value >> unit; - if (key == "MemTotal:") total = value * 1024; // kB to bytes - else if (key == "MemAvailable:") available = value * 1024; - - if (total > 0 && available > 0) break; - } - - used = total - available; -} - -} // namespace collectors -} // namespace agent -} // namespace volta diff --git a/source/agent/src/collectors/ram_collector.h b/source/agent/src/collectors/ram_collector.h deleted file mode 100644 index 49fba22..0000000 --- a/source/agent/src/collectors/ram_collector.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef VOLTA_AGENT_SRC_COLLECTORS_RAM_COLLECTOR_H_ -#define VOLTA_AGENT_SRC_COLLECTORS_RAM_COLLECTOR_H_ - -#include "collectors/collector.h" - -namespace volta { -namespace agent { -namespace collectors { - -class RamCollector : public Collector { -public: - std::vector Collect() override; - -private: - void ReadStats(uint64_t& used, uint64_t& total); - - bool initialized_ = false; -}; - -} // namespace collectors -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_SRC_COLLECTORS_RAM_COLLECTOR_H_ diff --git a/source/agent/src/config/config.h b/source/agent/src/config/config.h deleted file mode 100644 index 91fb05f..0000000 --- a/source/agent/src/config/config.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef VOLTA_AGENT_CONFIG_CONFIG_H_ -#define VOLTA_AGENT_CONFIG_CONFIG_H_ - -#include -#include -#include -#include - -namespace volta { -namespace agent { -namespace config { - -namespace CollectorNames { - // CPU - static constexpr char const* kProcStat = "proc_stat"; - static constexpr char const* kCpuFreq = "cpu_freq"; - static constexpr char const* kRapl = "rapl"; - static constexpr char const* kZenPower = "zenpower"; - static constexpr char const* kPmu = "pmu"; - - // GPU - static constexpr char const* kNvml = "nvml"; - static constexpr char const* kDcgm = "dcgm"; - static constexpr char const* kRocm = "rocm"; - static constexpr char const* kLevelZero = "level_zero"; - - // RAM - static constexpr char const* kMemInfo = "mem_info"; - static constexpr char const* kVmStat = "vm_stat"; - - // Disc and Network (I/O) - static constexpr char const* kDiskStats = "disk_stats"; - static constexpr char const* kNetDev = "net_dev"; -} - -struct CollectorConfig { - bool enabled = false; - std::map metrics; -}; - -struct Config { - static constexpr int32_t kDefaultIntervalMs = 500; - static constexpr int32_t kDefaultAffinity = -1; - static constexpr char const* kDefaultServerAddress = "localhost"; - static constexpr uint16_t kDefaultServerPort = 50051; - - std::chrono::milliseconds collection_interval = std::chrono::milliseconds(kDefaultIntervalMs); - int32_t core_affinity = kDefaultAffinity; - - std::string server_address = kDefaultServerAddress; - uint16_t server_port = kDefaultServerPort; - - std::map collectors; -}; - -} // namespace config -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_CONFIG_CONFIG_H_ diff --git a/source/agent/src/config/config_loader.cc b/source/agent/src/config/config_loader.cc deleted file mode 100644 index 37a44ff..0000000 --- a/source/agent/src/config/config_loader.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "config/config_loader.h" - -namespace volta { -namespace agent { -namespace config { - -Config ConfigLoader::LoadConfig() { - Config config; - - CollectorConfig nvml_collector; - nvml_collector.enabled = true; - nvml_collector.metrics = { - {"gpu_utilization", true}, - {"memory_utilization", true}, - {"temperature", true}, - }; - config.collectors[CollectorNames::kNvml] = nvml_collector; - - CollectorConfig proc_stat_config; - proc_stat_config.enabled = true; - proc_stat_config.metrics["cpu_usage_percent"] = true; - config.collectors[CollectorNames::kProcStat] = proc_stat_config; - - return config; -} - -Config ConfigLoader::LoadConfig(const std::filesystem::path& filepath) { - // ignore for POC - return LoadConfig(); -} - -} // namespace config -} // namespace agent -} // namespace volta diff --git a/source/agent/src/config/config_loader.h b/source/agent/src/config/config_loader.h deleted file mode 100644 index d447dca..0000000 --- a/source/agent/src/config/config_loader.h +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef VOLTA_AGENT_CONFIG_CONFIG_LOADER_H_ -#define VOLTA_AGENT_CONFIG_CONFIG_LOADER_H_ - -#include -#include - -#include "config/config.h" - -namespace volta { -namespace agent { -namespace config { - -class ConfigLoader { -public: - static Config LoadConfig(); - static Config LoadConfig(const std::filesystem::path& filepath); - -private: -}; - -} // namespace config -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_CONFIG_CONFIG_LOADER_H_ diff --git a/source/agent/src/main.cc b/source/agent/src/main.cc deleted file mode 100644 index e916d79..0000000 --- a/source/agent/src/main.cc +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "scheduler.h" -#include "config/config_loader.h" -#include "config/config.h" -#include "platform/platform_detector.h" -#include "collectors/collector.h" -#include "collectors/ram_collector.h" -#include "collectors/nvml_collector.h" -#include "collectors/proc_stat_collector.h" - -using namespace volta::agent; - -int main() { - try { - auto config = config::ConfigLoader::LoadConfig(); - - platform::PlatformDetector detector; - auto hw = detector.Detect(); - detector.PrintDetectedInfo(hw); - - std::vector> active_collectors; - - active_collectors.push_back(std::make_unique()); - - active_collectors.push_back(std::make_unique()); - - for (const auto& gpu : hw.gpus) { - if (gpu.vendor == platform::GpuVendor::NVIDIA) { - auto nvml = std::make_unique(); - if (nvml->Init()) { - active_collectors.push_back(std::move(nvml)); - } - } - } - - Scheduler scheduler(config, std::move(active_collectors)); - scheduler.Run(); - - } catch (const std::exception& e) { - std::cerr << "CRITICAL ERROR: " << e.what() << std::endl; - return 1; - } - return 0; -} diff --git a/source/agent/src/metric.h b/source/agent/src/metric.h deleted file mode 100644 index f1847ff..0000000 --- a/source/agent/src/metric.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef VOLTA_AGENT_SRC_METRIC_H -#define VOLTA_AGENT_SRC_METRIC_H - -#include -#include - -namespace volta { -namespace agent { - -struct Metric { - std::string name; - double value; - int64_t timestamp; -}; - -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_SRC_METRIC_H diff --git a/source/agent/src/platform/cpu_info.h b/source/agent/src/platform/cpu_info.h deleted file mode 100644 index fe97391..0000000 --- a/source/agent/src/platform/cpu_info.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef VOLTA_AGENT_PLATFORM_CPU_INFO_H_ -#define VOLTA_AGENT_PLATFORM_CPU_INFO_H_ - -#include -#include - -namespace volta { -namespace agent { -namespace platform { - -enum class CpuVendor { - UNKNOWN, - AMD, - INTEL -}; - -struct CpuInfo { - CpuVendor vendor = CpuVendor::UNKNOWN; - - std::string model_name; - std::string architecture; - - // Topology information - uint8_t physical_cores = 0; - uint8_t logical_threads = 0; - uint8_t socket_count = 1; - - float base_frequency_mhz = 0.0; - float max_frequency_mhz = 0.0; - - uint64_t l1_cache_size_bytes = 0; - uint64_t l2_cache_size_bytes = 0; - uint64_t l3_cache_size_bytes = 0; - - float power_limit_watts = 0.0; - - bool has_rapl = false; // Support Intel RAPL - bool has_zenpower = false; // Support AMD zenpower/k10temp - bool has_pmu = false; // Access to Performance Monitoring Unit -}; - -} // namespace platform -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_PLATFORM_CPU_INFO_H_ diff --git a/source/agent/src/platform/gpu_info.h b/source/agent/src/platform/gpu_info.h deleted file mode 100644 index 9fb28c1..0000000 --- a/source/agent/src/platform/gpu_info.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef VOLTA_AGENT_PLATFORM_GPU_INFO_H_ -#define VOLTA_AGENT_PLATFORM_GPU_INFO_H_ - -#include -#include - -namespace volta { -namespace agent { -namespace platform { - -enum class GpuVendor { - UNKNOWN, - NVIDIA, - AMD, - INTEL -}; - -struct GpuInfo { - GpuVendor vendor = GpuVendor::UNKNOWN; - - std::string model_name; - - // Unique identifier (Domain:Bus:Device.Function) to differentiate multiple GPUs in one system - std::string pci_address; - std::string driver_version; - - uint64_t vram_total_bytes = 0; - float power_limit_watts = 0.0; -}; - -} // namespace platform -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_PLATFORM_GPU_INFO_H_ diff --git a/source/agent/src/platform/platform_detector.cc b/source/agent/src/platform/platform_detector.cc deleted file mode 100644 index 44b5908..0000000 --- a/source/agent/src/platform/platform_detector.cc +++ /dev/null @@ -1,128 +0,0 @@ -#include "platform/platform_detector.h" - -#include -#include -#include -#include -#include - -#ifdef HAVE_NVML -#include -#endif - -namespace volta { -namespace agent { -namespace platform { - -static std::string CpuVendorToString(CpuVendor v) { - switch(v) { - case CpuVendor::INTEL: return "Intel"; - case CpuVendor::AMD: return "AMD"; - default: return "Unknown"; - } -} - -static std::string GpuVendorToString(GpuVendor v) { - switch(v) { - case GpuVendor::NVIDIA: return "NVIDIA"; - case GpuVendor::AMD: return "AMD"; - case GpuVendor::INTEL: return "Intel"; - default: return "Unknown"; - } -} - -HardwareInfo PlatformDetector::Detect() { - HardwareInfo info; - - char hostname_buffer[256]; - if (gethostname(hostname_buffer, sizeof(hostname_buffer)) == 0) info.hostname = std::string(hostname_buffer); - else info.hostname = "unknown-host"; - - info.os_version = DetectOS(); - - std::ifstream cpuinfo("/proc/cpuinfo"); - std::string line; - while (std::getline(cpuinfo, line)) { - if (line.find("vendor_id") != std::string::npos) { - if (line.find("AuthenticAMD") != std::string::npos) info.cpu.vendor = CpuVendor::AMD; - else if (line.find("GenuineIntel") != std::string::npos) info.cpu.vendor = CpuVendor::INTEL; - } - if (line.find("model name") != std::string::npos) { - size_t pos = line.find(":"); - if (pos != std::string::npos) { - info.cpu.model_name = line.substr(pos + 2); - } - break; - } - } - -#ifdef HAVE_NVML - if (nvmlInit() == NVML_SUCCESS) { - unsigned int device_count = 0; - nvmlDeviceGetCount(&device_count); - - for (unsigned int i = 0; i < device_count; ++i) { - nvmlDevice_t device; - nvmlDeviceGetHandleByIndex(i, &device); - - char name[64]; - if (nvmlDeviceGetName(device, name, sizeof(name)) == NVML_SUCCESS) { - GpuInfo gpu; - gpu.vendor = GpuVendor::NVIDIA; - gpu.model_name = std::string(name); - info.gpus.push_back(gpu); - } - } - nvmlShutdown(); - } -#endif - - return info; -} - -void PlatformDetector::PrintDetectedInfo(const HardwareInfo& info) { - std::cout << "[" << typeid(*this).name() << "] Hardware Detection Result:\n"; - std::cout << " > Host: " << info.hostname << "\n"; - std::cout << " > OS: " << info.os_version << "\n"; - std::cout << " > CPU: " << CpuVendorToString(info.cpu.vendor) << "\n" - << " | Model: " << info.cpu.model_name << "\n"; - - if (info.gpus.empty()) { - std::cout << " > GPU: None detected (or NVML disabled)\n"; - } else { - for (const auto& gpu : info.gpus) { - std::cout << " > GPU: " << GpuVendorToString(gpu.vendor) << "\n" - << " | Model: " << gpu.model_name << "\n"; - } - } - std::cout << "--------------------------------------------------\n"; - std::cout.flush(); - std::cin.get(); -} - -std::string PlatformDetector::DetectOS() { - std::ifstream file("/etc/os-release"); - - if (!file.is_open()) { - return "Linux (Unknown - Cannot open /etc/os-release)"; - } - - std::string line; - while (std::getline(file, line)) { - if (line.find("PRETTY_NAME=") == 0) { - std::string value = line.substr(12); - - if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { - value = value.substr(1, value.size() - 2); - } - - return value; - } - } - - return "Linux (Unknown - Tag not found)"; -} - -} // namespace platform -} // namespace agent -} // namespace volta diff --git a/source/agent/src/scheduler.cc b/source/agent/src/scheduler.cc deleted file mode 100644 index 6bb88bd..0000000 --- a/source/agent/src/scheduler.cc +++ /dev/null @@ -1,57 +0,0 @@ -#include "scheduler.h" - -#include -#include -#include - -namespace volta { -namespace agent { - -Scheduler::Scheduler( - const config::Config& config, - std::vector>&& collectors) - : config_(config) - , collectors_(std::move(collectors)) {} - -void Scheduler::Run() { - std::cout << "[" << typeid(*this).name() << "] Starting collection loop (Interval: " - << config_.collection_interval.count() << "ms)..." << std::endl; - - std::this_thread::sleep_for(config_.collection_interval); - - while (true) { - std::vector batch; - for (const auto& collector : collectors_) { - auto metrics = collector->Collect(); - batch.insert(batch.end(), metrics.begin(), metrics.end()); - } - - PrintDashboard(batch); - - std::this_thread::sleep_for(config_.collection_interval); - } -} - -void Scheduler::PrintDashboard(const std::vector& metrics) { - std::cout << "\033[2J\033[1;1H"; // ansi clean screen, move cursor to top-left - - std::cout << "===============================================\n"; - std::cout << " VOLTA AGENT v0.1 (POC) - ACTIVE MONITOR \n"; - std::cout << "===============================================\n"; - - std::cout << std::left << std::setw(30) << "METRIC NAME" << "VALUE\n"; - std::cout << "-----------------------------------------------\n"; - - for (const auto& m : metrics) { - std::cout << std::left << std::setw(30) << m.name - << std::fixed << std::setprecision(2) << m.value << "\n"; - } - - std::cout << "-----------------------------------------------\n"; - std::cout << "Data points collected: " << metrics.size() << "\n"; - std::cout << "Press Ctrl+C to exit." << "\n"; - std::cout.flush(); -} - -} // namespace agent -} // namespace volta diff --git a/source/agent/src/scheduler.h b/source/agent/src/scheduler.h deleted file mode 100644 index d68a3af..0000000 --- a/source/agent/src/scheduler.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef VOLTA_AGENT_SRC_SCHEDULER_H_ -#define VOLTA_AGENT_SRC_SCHEDULER_H_ - -#include -#include - -#include "metric.h" -#include "collectors/collector.h" -#include "config/config.h" - -namespace volta { -namespace agent { - -class Scheduler { -public: - explicit Scheduler( - const config::Config& config, - std::vector>&& collectors); - - void Run(); - -private: - void PrintDashboard(const std::vector& metrics); - - std::vector> collectors_; - const config::Config& config_; -}; - -} // namespace agent -} // namespace volta - -#endif // VOLTA_AGENT_SRC_SCHEDULER_H_ diff --git a/source/dashboard/README.md b/source/dashboard/README.md deleted file mode 100644 index 75b67d4..0000000 --- a/source/dashboard/README.md +++ /dev/null @@ -1 +0,0 @@ -## Dashboard diff --git a/source/server/README.md b/source/server/README.md deleted file mode 100644 index fa1f266..0000000 --- a/source/server/README.md +++ /dev/null @@ -1 +0,0 @@ -## Server diff --git a/sources/agent/.gitignore b/sources/agent/.gitignore new file mode 100644 index 0000000..6892fd2 --- /dev/null +++ b/sources/agent/.gitignore @@ -0,0 +1,2 @@ +build/* +gen/* diff --git a/sources/agent/CMakeLists.txt b/sources/agent/CMakeLists.txt new file mode 100644 index 0000000..cc18f66 --- /dev/null +++ b/sources/agent/CMakeLists.txt @@ -0,0 +1,176 @@ +cmake_minimum_required(VERSION 3.16) + +project(volta LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "13.0") + message(FATAL_ERROR "GCC 13+ required (found ${CMAKE_CXX_COMPILER_VERSION})") +endif() + +# --- Dependencies --- +find_package(protobuf CONFIG REQUIRED) +find_package(gRPC CONFIG REQUIRED) +find_package(fmt CONFIG REQUIRED) +find_package(tomlplusplus CONFIG REQUIRED) +find_package(Threads REQUIRED) + +# --- Proto / gRPC code generation --- +set(PROTO_DIR ${CMAKE_SOURCE_DIR}/../proto) + +get_target_property(GRPC_CPP_PLUGIN gRPC::grpc_cpp_plugin LOCATION) + +set(PROTO_FILES + ${PROTO_DIR}/volta.proto + ${PROTO_DIR}/types/metric.proto + ${PROTO_DIR}/types/transport.proto + ${PROTO_DIR}/types/agent.proto +) + +set(PROTO_SRCS "") +set(PROTO_HDRS "") + +foreach(PROTO_FILE ${PROTO_FILES}) + get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE) + get_filename_component(PROTO_SUBDIR ${PROTO_FILE} DIRECTORY) + file(RELATIVE_PATH PROTO_REL ${PROTO_DIR} ${PROTO_SUBDIR}) + + set(OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/${PROTO_REL}") + file(MAKE_DIRECTORY ${OUT_DIR}) + + list(APPEND PROTO_SRCS "${OUT_DIR}/${PROTO_NAME}.pb.cc") + list(APPEND PROTO_SRCS "${OUT_DIR}/${PROTO_NAME}.grpc.pb.cc") + list(APPEND PROTO_HDRS "${OUT_DIR}/${PROTO_NAME}.pb.h") + list(APPEND PROTO_HDRS "${OUT_DIR}/${PROTO_NAME}.grpc.pb.h") + + add_custom_command( + OUTPUT + "${OUT_DIR}/${PROTO_NAME}.pb.cc" + "${OUT_DIR}/${PROTO_NAME}.pb.h" + "${OUT_DIR}/${PROTO_NAME}.grpc.pb.cc" + "${OUT_DIR}/${PROTO_NAME}.grpc.pb.h" + COMMAND protobuf::protoc + ARGS + --cpp_out="${CMAKE_CURRENT_BINARY_DIR}" + --grpc_out="${CMAKE_CURRENT_BINARY_DIR}" + --plugin=protoc-gen-grpc="${GRPC_CPP_PLUGIN}" + -I "${PROTO_DIR}" + "${PROTO_FILE}" + DEPENDS "${PROTO_FILE}" gRPC::grpc_cpp_plugin + COMMENT "Generating C++ code from ${PROTO_FILE}..." + ) +endforeach() + +add_library(volta_proto STATIC ${PROTO_SRCS} ${PROTO_HDRS}) + +target_include_directories(volta_proto PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + +target_link_libraries(volta_proto PUBLIC + protobuf::libprotobuf + gRPC::grpc++ + gRPC::grpc++_reflection +) + +# --- Agent executable --- +add_executable(volta) + +target_compile_options(volta PRIVATE -std=c++20) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + message(STATUS "Debug build - adding DEBUG macro") + target_compile_definitions(volta PRIVATE DEBUG) +endif() + +file(GLOB_RECURSE AGENT_SOURCES CONFIGURE_DEPENDS "src/*.cc") +target_sources(volta PRIVATE ${AGENT_SOURCES}) + +target_include_directories(volta PRIVATE + src + ${CMAKE_SOURCE_DIR}/libs/include/nvidia +) + +target_link_libraries(volta PRIVATE + volta_proto + Threads::Threads + fmt::fmt + tomlplusplus::tomlplusplus + ${CMAKE_DL_LIBS} +) + +target_compile_definitions(volta PRIVATE NVML_NO_UNVERSIONED_FUNC_DEFS) + +# --- Config file --- +if(NOT EXISTS ${CMAKE_SOURCE_DIR}/agent.conf) + if(EXISTS ${CMAKE_SOURCE_DIR}/agent.example.conf) + message(STATUS "agent.conf not found - copying from agent.example.conf") + file(COPY ${CMAKE_SOURCE_DIR}/agent.example.conf + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + file(RENAME ${CMAKE_CURRENT_BINARY_DIR}/agent.example.conf + ${CMAKE_CURRENT_BINARY_DIR}/agent.conf) + else() + message(WARNING "Neither agent.conf nor agent.example.conf found") + endif() +else() + add_custom_command( + TARGET volta POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/agent.conf + ${CMAKE_CURRENT_BINARY_DIR}/agent.conf + COMMENT "Copying agent.conf..." + ) +endif() + +add_custom_command( + TARGET volta POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rm -f $/voltad + COMMAND ${CMAKE_COMMAND} -E create_hardlink + $ + $/voltad + COMMENT "Creating hardlink voltad..." +) + +# --- Formatting --- +find_program(CLANG_FORMAT_EXE clang-format) + +if(CLANG_FORMAT_EXE) + add_custom_target(format + COMMAND find src -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} -i -style=Google + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Running clang-format on agent sources..." + VERBATIM + ) + add_custom_target(check-format + COMMAND find src -name "*.cc" -o -name "*.h" | xargs ${CLANG_FORMAT_EXE} --dry-run --Werror -style=Google + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Checking agent source formatting..." + VERBATIM + ) +else() + message(WARNING "clang-format not found - 'make format' unavailable") +endif() + +# TODO: find rocm_smi and oneapi level_zero +# TODO: find PMU (libpfm) + +# --- Scripts --- +find_program(GIT_EXE git) +if(GIT_EXE) + execute_process( + COMMAND ${GIT_EXE} rev-parse --show-toplevel + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_ROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(EXISTS ${GIT_ROOT}/.git) + configure_file( + ${GIT_ROOT}/scripts/hooks/pre-commit + ${GIT_ROOT}/.git/hooks/pre-commit + COPYONLY + ) + file(CHMOD ${GIT_ROOT}/.git/hooks/pre-commit + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) + message(STATUS "Git pre-commit hook installed") + endif() +endif() diff --git a/sources/agent/CMakePresets.json b/sources/agent/CMakePresets.json new file mode 100644 index 0000000..36f92ff --- /dev/null +++ b/sources/agent/CMakePresets.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "default", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "default", + "configurePreset": "default" + } + ] +} \ No newline at end of file diff --git a/sources/agent/README.md b/sources/agent/README.md new file mode 100644 index 0000000..b5ae934 --- /dev/null +++ b/sources/agent/README.md @@ -0,0 +1,92 @@ +# Agent + +Core maintainer: @FW-Nagorko + +## Development Setup + +Project is using CMake as build system and vcpkg in Manifest Mode for dependency management. + +### 1. Initial Requirements + +- **Linux** (Kernel 5.x.+) +- **C++20 Compiler** (GCC 13+/Clang 14+) +- **CMake** (3.16+) +- **Git** + +### 2. Vcpkg Installation + +```bash +# Clone vcpkg repository +git clone https://github.com/microsoft/vcpkg.git ~/vcpkg + +# Run bootstraping script +~/vcpkg/bootstrap-vcpkg.sh + +# (Optional) Set VCPKG_ROOT environment variable in .bashrc (or other rc) +# Otherway you will need to enter the path for every single build. +export VCPKG_ROOT=~/vcpkg +``` + +### 3. Build Project + +```bash +# Configurate the project, compile dependencies. +cmake -B build -S . --preset default + +# Compilation +cmake --build build +``` + +### 4. Run Agent + +```bash +# May require root/admin privileges to access RAPL/Affinity features. +./build/voltad +``` + +## Agent Configuration + +The agent reads its configuration from `agent.conf` in the current working directory. +If the file is missing, the agent starts with built-in defaults. + +You can use `sources/agent/agent.example.conf` as a starting point. + +### Supported keys + +- `core_affinity` — either `"all"` or an array of CPU indices/ranges + (for example `[2, "3-10"]`). +- `interval` — collection interval in milliseconds (`uint32`). +- `server_address` — IP address or resolvable hostname. +- `server_port` — TCP port in range `1..65535`. +- `metrics` — a string or an array of strings. + Metric names may be written with or without the `METRIC_TYPE_` prefix. +- `time_window` — buffering window in seconds (`float`), and it must be + greater than the collection interval. + +### Example + +```toml +core_affinity = [2, "3-10"] +interval = 500 +server_address = "localhost" +server_port = 5000 +time_window = 2.0 + +metrics = [ + "METRIC_TYPE_CPU_POWER_PACKAGE", + "GPU_POWER", +] +``` + +Unknown top-level keys are ignored with a warning. + +### FAQ +**Q: Do I have to enter `vcpkg install`?** +**A:** No, project is working in Manifest Mode. CMake automatically reads the `vcpkg.json` file and installs whats needed in isolated environment inside build directory. + +**Q: I changed `vcpkg.json`, but build see no difference.** +**A:** Clean up CMake cache, and build project once again: +```bash +rm -rf build +cmake -B build -S . +``` diff --git a/sources/agent/agent.example.conf b/sources/agent/agent.example.conf new file mode 100644 index 0000000..ae0a6b4 --- /dev/null +++ b/sources/agent/agent.example.conf @@ -0,0 +1,7 @@ +core_affinity = [2, "3-10"] + +# Collect CPU and GPU power metrics +metrics = [ + "METRIC_TYPE_CPU_POWER_PACKAGE", + "METRIC_TYPE_GPU_POWER", +] diff --git a/libs/include/nvidia/nvml.h b/sources/agent/libs/include/nvidia/nvml.h similarity index 100% rename from libs/include/nvidia/nvml.h rename to sources/agent/libs/include/nvidia/nvml.h diff --git a/sources/agent/src/buffer.cc b/sources/agent/src/buffer.cc new file mode 100644 index 0000000..fbff1e9 --- /dev/null +++ b/sources/agent/src/buffer.cc @@ -0,0 +1,193 @@ +#include "buffer.h" + +#include +#include +#include +#include + +namespace volta { +namespace agent { + +namespace { + +constexpr uint64_t kFnvOffset = 1469598103934665603ULL; +constexpr uint64_t kFnvPrime = 1099511628211ULL; + +uint64_t HashCombineBytes(uint64_t hash, const void* data, size_t size) { + const auto* bytes = static_cast(data); + for (size_t i = 0; i < size; ++i) { + hash ^= bytes[i]; + hash *= kFnvPrime; + } + return hash; +} + +} // namespace + +size_t BufferKeyHash::operator()(const BufferKey& key) const noexcept { + uint64_t hash = kFnvOffset; + hash = HashCombineBytes(hash, &key.metric_type, sizeof(key.metric_type)); + hash = HashCombineBytes(hash, &key.pci_domain, sizeof(key.pci_domain)); + hash = HashCombineBytes(hash, &key.pci_bus, sizeof(key.pci_bus)); + hash = HashCombineBytes(hash, &key.pci_device, sizeof(key.pci_device)); + hash = HashCombineBytes(hash, &key.pci_function, sizeof(key.pci_function)); + hash = HashCombineBytes(hash, &key.socket_index, sizeof(key.socket_index)); + hash = HashCombineBytes(hash, &key.core_index, sizeof(key.core_index)); + hash = HashCombineBytes(hash, &key.ifindex, sizeof(key.ifindex)); + hash = HashCombineBytes(hash, &key.disk_major, sizeof(key.disk_major)); + hash = HashCombineBytes(hash, &key.disk_minor, sizeof(key.disk_minor)); + return static_cast(hash); +} + +SeriesBuffer::SeriesBuffer(size_t capacity) : mutex_() { + SetCapacity(capacity); +} + +void SeriesBuffer::SetCapacity(size_t capacity) { + std::lock_guard lock(mutex_); + + capacity_ = capacity; + samples_.clear(); + samples_.reserve(capacity_); + samples_.resize(capacity_); + head_ = 0; +} + +void SeriesBuffer::Push(const Sample& sample) { + std::lock_guard lock(mutex_); + if (capacity_ == 0) return; + + if (head_ - tail_ >= capacity_) tail_++; // drop oldest unsent + + samples_[head_ % capacity_] = sample; + head_++; +} + +std::optional SeriesBuffer::Latest() const { + std::lock_guard lock(mutex_); + + if (head_ == 0) return std::nullopt; + return samples_[(head_ - 1) % capacity_]; +} + +size_t SeriesBuffer::GetNextSnapshotSize() const { + std::shared_lock lock(mutex_); + return head_ - tail_; +} +SeriesBuffer::Snapshot SeriesBuffer::GetSnapshot() const { + std::shared_lock lock(mutex_); + + std::vector result; + result.reserve(head_ - tail_); + + for (size_t i = tail_; i < head_; ++i) { + result.push_back(samples_[i % capacity_]); + } + + return {result, head_}; +} + +void SeriesBuffer::AckSnapshot(const SeriesBuffer::Snapshot& snapshot) { + std::lock_guard lock(mutex_); + + if (snapshot.end > tail_) tail_ = snapshot.end; +} + +MetricsBuffer::MetricsBuffer(const config::Config& cfg) + : mutex_(), + capacity_per_series_(cfg.buffered_time_window / cfg.collection_interval) { +} + +void MetricsBuffer::SetCapacityPerSeries(size_t capacity) { + std::lock_guard lock(mutex_); + capacity_per_series_ = capacity; + for (auto& [_, buffer] : series_) { + buffer->SetCapacity(capacity_per_series_); + } +} + +std::shared_ptr MetricsBuffer::GetBuffer( + const BufferKey& key) const { + std::shared_lock lock(mutex_); + auto it = series_.find(key); + return it != series_.end() ? it->second : nullptr; +} + +void MetricsBuffer::AddSample(const BufferKey& key, const Sample& sample) { + auto [it, inserted] = series_.emplace(key, nullptr); + if (inserted) { + it->second = std::make_unique(capacity_per_series_); + keys_.push_back(key); + } + it->second->Push(sample); +} + +void MetricsBuffer::AddMetric(const Metric& metric) { + if (metric.type == MetricType::METRIC_TYPE_UNSPECIFIED) return; + AddSample(MakeBufferKey(metric), + Sample{.timestamp_ns = metric.timestamp, .value = metric.value}); +} + +void MetricsBuffer::AddMetrics(const std::vector& metrics) { + std::lock_guard lock(mutex_); + for (const auto& metric : metrics) AddMetric(metric); +} + +std::optional MetricsBuffer::Latest(const BufferKey& key) const { + std::lock_guard lock(mutex_); + + auto it = series_.find(key); + if (it == series_.end()) return std::nullopt; + return it->second->Latest(); +} + +std::vector> MetricsBuffer::LatestSamples() const { + std::lock_guard lock(mutex_); + + std::vector> result; + result.reserve(series_.size()); + for (const auto& [key, series] : series_) { + auto latest = series->Latest(); + if (latest.has_value()) { + result.emplace_back(key, *latest); + } + } + return result; +} + +std::vector MetricsBuffer::GetAllKeys() const { + std::lock_guard lock(mutex_); + return keys_; +} + +BufferKey MetricsBuffer::MakeBufferKey(const Metric& metric) { + BufferKey key; + key.metric_type = static_cast(metric.type); + if (!metric.devId.has_value()) return key; + + const auto& device = metric.devId.value(); + std::visit( + [&](const auto& id) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + key.pci_domain = static_cast(id.pci_domain()); + key.pci_bus = static_cast(id.pci_bus()); + key.pci_device = static_cast(id.pci_device()); + key.pci_function = static_cast(id.pci_function()); + } else if constexpr (std::is_same_v) { + key.socket_index = static_cast(id.socket_index()); + key.core_index = static_cast(id.core_index()); + } else if constexpr (std::is_same_v) { + key.ifindex = id.ifindex(); + } else if constexpr (std::is_same_v) { + key.disk_major = id.major(); + key.disk_minor = id.minor(); + } + }, + device); + + return key; +} + +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/buffer.h b/sources/agent/src/buffer.h new file mode 100644 index 0000000..0fe06d7 --- /dev/null +++ b/sources/agent/src/buffer.h @@ -0,0 +1,121 @@ +#ifndef VOLTA_AGENT_SRC_BUFFER_H_ +#define VOLTA_AGENT_SRC_BUFFER_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config/config.h" + +namespace volta { +namespace agent { + +struct Sample { + int64_t timestamp_ns = 0; + double value = 0.0; +}; + +struct BufferKey { + int32_t metric_type = 0; + + // GPU fields (valid when metric_type in [200, 299]) + uint16_t pci_domain = 0; + uint8_t pci_bus = 0; + uint8_t pci_device = 0; + uint8_t pci_function = 0; + + // CPU fields (valid when metric_type in [100, 199]) + uint8_t socket_index = 0; + uint16_t core_index = 0; + + // Network fields (valid when metric_type in [500, 599]) + uint32_t ifindex = 0; + + // Disk fields (valid when metric_type in [400, 499]) + uint32_t disk_major = 0; + uint32_t disk_minor = 0; + + bool operator==(const BufferKey&) const = default; +}; + +static_assert(std::is_standard_layout_v); + +struct BufferKeyHash { + size_t operator()(const BufferKey& key) const noexcept; +}; + +class SeriesBuffer { + public: + explicit SeriesBuffer(size_t capacity = 0); + + using Snapshot = struct { + std::vector samples; + size_t end; + }; + + void Push(const Sample& sample); + std::optional Latest() const; + + // Returns the number of unsent samples + size_t GetNextSnapshotSize() const; + + // Snapshots all of the unsent samples + Snapshot GetSnapshot() const; + + // Marks all the samples from the snapshot as sent + void AckSnapshot(const Snapshot& snapshot_end); + + void SetCapacity(size_t capacity); + size_t Capacity() const { return capacity_; } + size_t Size() const { return samples_.size(); } + bool Empty() const { return samples_.empty(); } + size_t GetHead() const { return head_; } + size_t GetTail() const { return tail_; } + + private: + mutable std::shared_mutex mutex_; + size_t capacity_ = 0; + std::vector samples_; + size_t head_ = 0; + size_t tail_ = 0; +}; + +class MetricsBuffer { + public: + explicit MetricsBuffer(const config::Config& cfg); + + size_t CapacityPerSeries() const { return capacity_per_series_; } + + void AddMetrics(const std::vector& metrics); + std::shared_ptr GetBuffer(const BufferKey& key) const; + std::vector GetAllKeys() const; + std::vector> LatestSamples() const; + + static BufferKey MakeBufferKey(const Metric& metric); + + private: + using SeriesMap = std::unordered_map, + BufferKeyHash>; + + void SetCapacityPerSeries(size_t capacity); + std::optional Latest(const BufferKey& key) const; + void AddSample(const BufferKey& key, const Sample& sample); + void AddMetric(const Metric& metric); + + mutable std::shared_mutex mutex_; + size_t capacity_per_series_ = 0; + SeriesMap series_; + std::vector keys_; +}; + +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_BUFFER_H_ diff --git a/sources/agent/src/client/connect_reactor.cc b/sources/agent/src/client/connect_reactor.cc new file mode 100644 index 0000000..315b0a4 --- /dev/null +++ b/sources/agent/src/client/connect_reactor.cc @@ -0,0 +1,118 @@ +#include "connect_reactor.h" + +#include +#include +#include + +#include "imessage_handler.h" + +extern volatile sig_atomic_t g_running; + +namespace volta { +namespace agent { +namespace client { + +ConnectReactor::ConnectReactor(IMessageHandler* handler, const std::string& id, + ::volta::VoltaCollector::Stub* stub) + : handler_(handler) { + std::cout << "Starting ConnectReactor for agent " << id << std::endl; + + context_.AddMetadata("agent-id", id); + stub->async()->Connect(&context_, this); + + EnqueueWrite(CreateMessage(::volta::MessageType::MESSAGE_ID)); + + StartRead(&res_); + StartCall(); +} + +void ConnectReactor::OnWriteDone(bool ok) { + if (!ok) { + std::cerr << "Write failed" << std::endl; + return; + } + + std::lock_guard l(mu_); + + writerqu_.pop(); + writing_ = false; + std::cout << "[" << std::chrono::system_clock::now() << "]" + << " Finished writing message to server, queue size: " + << writerqu_.size() << std::endl; + + Write(); +} + +void ConnectReactor::OnReadDone(bool ok) { + if (!ok) { + std::cerr << "Read failed" << std::endl; + return; + } + + std::cout << "[" << std::chrono::system_clock::now() << "]" + << " Message from server: " + << ::volta::MessageType_Name(res_.type()) << std::endl; + handler_->OnMessage(res_); + StartRead(&res_); +} + +void ConnectReactor::OnDone(const grpc::Status& status) { + std::unique_lock l(mu_); + status_ = status; + done_ = true; + cv_.notify_one(); +} + +grpc::Status ConnectReactor::Await() { + std::unique_lock l(mu_); + while (!done_) { + if (!g_running) { + context_.TryCancel(); + } + cv_.wait_for(l, std::chrono::milliseconds(200), [this] { return done_; }); + } + + return std::move(status_); +} + +void ConnectReactor::EnqueueWrite(::volta::ControlMessage msg) { + std::cout << "[" << std::chrono::system_clock::now() << "]" + << " Enqueuing message to server: " + << ::volta::MessageType_Name(msg.type()) << std::endl; + + std::lock_guard l(mu_); + + writerqu_.push(std::move(msg)); + Write(); +} + +// Must be called with wmu_ locked. +void ConnectReactor::Write() { + if (writing_ || writerqu_.empty()) { + return; + } + + std::cout << "[" << std::chrono::system_clock::now() << "]" + << " Starting write of message to server: " + << ::volta::MessageType_Name(writerqu_.front().type()) << std::endl; + + writing_ = true; + auto& msg = writerqu_.front(); + StartWrite(&msg); +} + +::volta::ControlMessage ConnectReactor::CreateMessage( + ::volta::MessageType type, const std::string& payload) { + ::volta::ControlMessage msg; + msg.set_type(type); + + if (type == ::volta::MessageType::MESSAGE_ERROR) { + msg.set_payload(payload); + } + + return msg; +} + +} // namespace client +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/client/connect_reactor.h b/sources/agent/src/client/connect_reactor.h new file mode 100644 index 0000000..dc4fc3e --- /dev/null +++ b/sources/agent/src/client/connect_reactor.h @@ -0,0 +1,61 @@ +#ifndef VOLTA_AGENT_CLIENT_CONNECT_REACTOR_H_ +#define VOLTA_AGENT_CLIENT_CONNECT_REACTOR_H_ + +#include +#include +#include + +#include "iwriter.h" +#include "volta.grpc.pb.h" + +namespace volta { +namespace agent { +namespace client { + +class IMessageHandler; + +class ConnectReactor final + : public grpc::ClientBidiReactor<::volta::ControlMessage, + ::volta::ControlMessage>, + public IWriter<::volta::ControlMessage> { + public: + explicit ConnectReactor(IMessageHandler* handler, const std::string& id, + ::volta::VoltaCollector::Stub* stub); + ~ConnectReactor() override { + std::cout << "ConnectReactor destroyed for agent " + << context_.GetServerInitialMetadata().find("agent-id")->second + << std::endl; + } + + // ClientBidiReactor + void OnWriteDone(bool ok) override; + void OnReadDone(bool ok) override; + void OnDone(const grpc::Status& status) override; + + // IWriter + void EnqueueWrite(::volta::ControlMessage msg) override; + + ::volta::ControlMessage CreateMessage(::volta::MessageType type, + const std::string& payload = ""); + grpc::Status Await(); + + private: + void Write() override; // IWriter + + IMessageHandler* handler_; + ::volta::VoltaCollector::Stub* stub_; + grpc::ClientContext context_; + + ::volta::ControlMessage res_; + + std::mutex mu_; + grpc::Status status_; + std::condition_variable cv_; + bool done_ = false; +}; + +} // namespace client +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CLIENT_CONNECT_REACTOR_H_ \ No newline at end of file diff --git a/sources/agent/src/client/imessage_handler.h b/sources/agent/src/client/imessage_handler.h new file mode 100644 index 0000000..7a338bb --- /dev/null +++ b/sources/agent/src/client/imessage_handler.h @@ -0,0 +1,20 @@ +#ifndef VOLTA_AGENT_CLIENT_IMESSAGE_HANDLER_H_ +#define VOLTA_AGENT_CLIENT_IMESSAGE_HANDLER_H_ + +#include "volta.grpc.pb.h" + +namespace volta { +namespace agent { +namespace client { + +class IMessageHandler { + public: + virtual ~IMessageHandler() = default; + virtual void OnMessage(const ::volta::ControlMessage& msg) = 0; +}; + +} // namespace client +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CLIENT_IMESSAGE_HANDLER_H_ \ No newline at end of file diff --git a/sources/agent/src/client/ireaderwriter.h b/sources/agent/src/client/ireaderwriter.h new file mode 100644 index 0000000..ff4abef --- /dev/null +++ b/sources/agent/src/client/ireaderwriter.h @@ -0,0 +1,40 @@ +#ifndef VOLTA_AGENT_CLIENT_IREADER_H +#define VOLTA_AGENT_CLIENT_IREADER_H + +#include +#include + +#include "iwriter.h" + +namespace volta { +namespace agent { +namespace client { + +template +class IReaderWriter : public IWriter { + public: + virtual ~IReaderWriter() = default; + virtual void BindMessage(const K& key, V msg) = 0; + virtual void UnbindMessage(const K& key) = 0; + virtual void EnqueueWrite(V msg) override = 0; + + V* GetMessage(const K& key) { + std::lock_guard l(mu_); + auto it = readermap_.find(key); + if (it != readermap_.end()) { + return &it->second; + } + return nullptr; + } + + protected: + std::unordered_map readermap_; + T msg_; + std::mutex rwmu_; +}; + +} // namespace client +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CLIENT_IREADER_H \ No newline at end of file diff --git a/sources/agent/src/client/iwriter.h b/sources/agent/src/client/iwriter.h new file mode 100644 index 0000000..f0a8166 --- /dev/null +++ b/sources/agent/src/client/iwriter.h @@ -0,0 +1,28 @@ +#ifndef VOLTA_AGENT_CLIENT_IWRITER_H +#define VOLTA_AGENT_CLIENT_IWRITER_H + +#include +#include + +namespace volta { +namespace agent { +namespace client { + +template +class IWriter { + public: + virtual ~IWriter() = default; + virtual void EnqueueWrite(T msg) = 0; + + protected: + virtual void Write() = 0; + + std::queue writerqu_; + bool writing_ = false; +}; + +} // namespace client +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CLIENT_IWRITER_H \ No newline at end of file diff --git a/sources/agent/src/client/stream_metrics_reactor.cc b/sources/agent/src/client/stream_metrics_reactor.cc new file mode 100644 index 0000000..2f8eb35 --- /dev/null +++ b/sources/agent/src/client/stream_metrics_reactor.cc @@ -0,0 +1,186 @@ +#include "stream_metrics_reactor.h" + +namespace volta { +namespace agent { +namespace client { + +StreamMetricsReactor::StreamMetricsReactor( + ::volta::VoltaCollector::Stub* stub, const std::string& id, + std::shared_ptr<::volta::agent::MetricsBuffer> buffer, + OnDoneCallback on_done) + : on_done_(std::move(on_done)), buffer_(buffer) { + std::cout << "Starting StreamMetricsReactor for agent " << id << std::endl; + context_.AddMetadata("agent-id", id); + stub->async()->StreamMetrics(&context_, this); + + StartCall(); + StartRead(&ack_msg_); + + poll_thread_ = std::jthread([this](std::stop_token st) { + // TODO: we should consider using a more efficient synchronization mechanism + // to avoid polling in a loop like this, e.g. condition variable or similar + // to wake up immediately when new metrics are available or when an ACK is + // received, instead of just sleeping for a fixed interval. + while (!st.stop_requested()) { + if (st.stop_requested()) break; + + bool has_pending = false; + { + std::lock_guard l(snapshot_mu_); + has_pending = !pending_snapshots_.empty(); + } + + if (!has_pending) { + EnqueueMetrics(); + + { + std::lock_guard l(snapshot_mu_); + has_pending = !pending_snapshots_.empty(); + } + + if (!has_pending) { + // No metrics in the buffer, wait a bit before polling again. + // TODO: use condition variable or similar to wake up immediately when + // new metrics are available??? + std::this_thread::sleep_for(std::chrono::seconds(1)); + } else { + Write(); + } + continue; + } + + // TODO: better handling of polling interval, maybe dynamic based on how + // fast metrics are produced and acked? + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + }); +} + +void StreamMetricsReactor::OnReadDone(bool ok) { + if (!ok) { + std::cerr << "Read failed" << std::endl; + return; + } + + BatchId batch_id = ack_msg_.batch_id(); + std::cout << "Received ACK for batch_id " << batch_id << std::endl; + + { + std::lock_guard l(snapshot_mu_); + auto it = pending_snapshots_.find(batch_id); + if (it != pending_snapshots_.end()) { + auto& [key, snapshot] = it->second; + buffer_->GetBuffer(key)->AckSnapshot(snapshot); + pending_snapshots_.erase(it); + // std::cout << "Acknowledged batch_id " << batch_id << ", pending + // snapshots: " << pending_snapshots_.size() + // << std::endl; + } else { + // TODO: Server may have returned an ACK before we added the snapshot to + // pending_snapshots_, so this might be a normal occurrence. We should + // consider adding some buffering or delay to handle this case more + // gracefully instead of just printing an error. + std::cout << "Received ACK for unknown batch_id " << batch_id + << std::endl; + } + } + + if (ack_msg_.has_error()) { + std::cerr << "Error: " << ack_msg_.error() << std::endl; + } + + StartRead(&ack_msg_); +} + +void StreamMetricsReactor::OnWriteDone(bool ok) { + if (!ok) { + std::cerr << "Write failed" << std::endl; + return; + } + + { + std::lock_guard l(write_mu_); + writerqu_.pop(); + writing_ = false; + } + + // std::cout << "Finished writing batch to server, queue size: " << + // writerqu_.size() << " " << pending_snapshots_.size() + // << std::endl; + + Write(); +} + +void StreamMetricsReactor::OnDone(const grpc::Status& status) { + std::cout << "StreamMetricsReactor finished: " << status.error_message() + << std::endl; + on_done_(status); +} + +void StreamMetricsReactor::Write() { + std::lock_guard l(write_mu_); + if (writing_ || writerqu_.empty()) { + return; + } + + writing_ = true; + auto& msg = writerqu_.front(); + StartWrite(&msg); +} + +void StreamMetricsReactor::EnqueueMetrics() { + auto keys = buffer_->GetAllKeys(); + + for (const auto& key : keys) { + EnqueueMetrics(key); + } +} + +void StreamMetricsReactor::EnqueueMetrics(const BufferKey& key) { + ::volta::agent::SeriesBuffer::Snapshot snapshot = + buffer_->GetBuffer(key)->GetSnapshot(); + + if (snapshot.samples.empty()) { + return; + } + + ::volta::MetricBatch batch; + for (const auto& sample : snapshot.samples) { + batch.add_timestamps_ns(sample.timestamp_ns); + batch.add_values(sample.value); + } + + ::volta::BatchHeader* header = batch.mutable_header(); + header->set_metric_type(static_cast<::volta::MetricType>(key.metric_type)); + + ::volta::DeviceID* device_id = header->mutable_device_id(); + + if (key.metric_type >= 100 && key.metric_type < 200) { + device_id->mutable_cpu()->set_socket_index(key.socket_index); + device_id->mutable_cpu()->set_core_index(key.core_index); + } else if (key.metric_type >= 200 && key.metric_type < 300) { + device_id->mutable_gpu()->set_pci_domain(key.pci_domain); + device_id->mutable_gpu()->set_pci_bus(key.pci_bus); + device_id->mutable_gpu()->set_pci_device(key.pci_device); + device_id->mutable_gpu()->set_pci_function(key.pci_function); + } else if (key.metric_type >= 400 && key.metric_type < 500) { + device_id->mutable_disk()->set_major(key.disk_major); + device_id->mutable_disk()->set_minor(key.disk_minor); + } else if (key.metric_type >= 500 && key.metric_type < 600) { + device_id->mutable_network()->set_ifindex(key.ifindex); + } + + batch.set_id(batch_id_counter_.fetch_add(1)); + + { + std::lock_guard l(snapshot_mu_); + pending_snapshots_.emplace(batch.id(), std::make_pair(key, snapshot)); + } + + std::lock_guard l(write_mu_); + writerqu_.push(std::move(batch)); +} + +} // namespace client +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/client/stream_metrics_reactor.h b/sources/agent/src/client/stream_metrics_reactor.h new file mode 100644 index 0000000..72a3115 --- /dev/null +++ b/sources/agent/src/client/stream_metrics_reactor.h @@ -0,0 +1,63 @@ +#ifndef VOLTA_AGENT_CLIENT_STREAM_METRICS_REACTOR_H_ +#define VOLTA_AGENT_CLIENT_STREAM_METRICS_REACTOR_H_ + +#include +#include +#include +#include + +#include "buffer.h" +#include "volta.grpc.pb.h" + +namespace volta { +namespace agent { +namespace client { + +using namespace ::volta::agent; + +class StreamMetricsReactor final + : public grpc::ClientBidiReactor<::volta::MetricBatch, ::volta::BatchAck> { + public: + using OnDoneCallback = std::function; + + StreamMetricsReactor(::volta::VoltaCollector::Stub* stub, + const std::string& id, + std::shared_ptr<::volta::agent::MetricsBuffer> buffer, + OnDoneCallback on_done); + ~StreamMetricsReactor() override = default; + + // ClientBidiReactor + void OnReadDone(bool ok) override; + void OnWriteDone(bool ok) override; + void OnDone(const grpc::Status& status) override; + + private: + using BatchId = uint64_t; + + void Write(); + void EnqueueMetrics(); + void EnqueueMetrics(const BufferKey& key); + + OnDoneCallback on_done_; + grpc::ClientContext context_; + std::shared_ptr buffer_; + + std::jthread poll_thread_; + std::unordered_map> + pending_snapshots_; + std::mutex snapshot_mu_; + + std::queue<::volta::MetricBatch> writerqu_; + bool writing_ = false; + std::mutex write_mu_; + + ::volta::BatchAck ack_msg_; + + std::atomic batch_id_counter_{0}; +}; + +} // namespace client +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CLIENT_STREAM_METRICS_REACTOR_H_ \ No newline at end of file diff --git a/sources/agent/src/client/volta_collector_client.cc b/sources/agent/src/client/volta_collector_client.cc new file mode 100644 index 0000000..c719897 --- /dev/null +++ b/sources/agent/src/client/volta_collector_client.cc @@ -0,0 +1,178 @@ +#include "volta_collector_client.h" + +#include + +#include + +#include "buffer.h" + +namespace volta { +namespace agent { +namespace client { + +std::filesystem::path Client::kUUIDFile = "agent.uuid"; + +Client::Client(std::shared_ptr channel, config::Config& config, + std::shared_ptr<::volta::agent::MetricsBuffer> buffer) + : stub_(::volta::VoltaCollector::NewStub(channel)), + config_(config), + buffer_(buffer) {} + +// TODO: consider making this more configurable, e.g. support TLS, custom +// options, etc. +std::shared_ptr Client::CreateChannel( + const std::string& address) { + auto channel = + grpc::CreateChannel(address, grpc::InsecureChannelCredentials()); + if (!channel) { + return nullptr; + } + + // TODO: consider making timeout configurable + if (!channel->WaitForConnected(std::chrono::system_clock::now() + + std::chrono::seconds(5))) { + return nullptr; + } + + return channel; +} + +bool Client::LoadUUID(std::string& out_uuid) { + std::ifstream f(kUUIDFile); + if (!f.is_open()) { + return false; + } + + std::getline(f, out_uuid); + return !out_uuid.empty(); +} + +void Client::SaveUUID(const std::string& uuid) { + if (uuid.empty()) { + std::cerr << "Cannot save empty UUID" << std::endl; + connect_reactor_->EnqueueWrite( + connect_reactor_->CreateMessage(::volta::MessageType::MESSAGE_ERROR, + "Empty UUID received from server")); + return; + } else if (std::filesystem::exists(kUUIDFile)) { + std::cerr << "UUID file already exists, refusing to overwrite" << std::endl; + connect_reactor_->EnqueueWrite( + connect_reactor_->CreateMessage(::volta::MessageType::MESSAGE_ERROR, + "UUID file already exists on agent")); + return; + } + + std::filesystem::path tmp = kUUIDFile; + tmp += ".tmp"; + + { + std::ofstream f(tmp, std::ios::trunc); + f << uuid; + f.flush(); + } + + std::filesystem::rename(tmp, kUUIDFile); + id_ = uuid; +} + +void Client::Connect() { + // TODO: handle case where UUID file exists but is empty or invalid + LoadUUID(id_); + + connect_reactor_ = std::make_unique(this, id_, stub_.get()); + grpc::Status status = connect_reactor_->Await(); + + if (!status.ok()) { + std::cout << "Stream closed with error: " << status.error_message() + << std::endl; + } +} + +void Client::OnMessage(const ::volta::ControlMessage& msg) { + std::cout << "Received message from server: " << msg.DebugString() + << std::endl; + + switch (msg.type()) { + // TODO: move pinging to grpc server itself + case ::volta::MessageType::MESSAGE_PING: { + // TODO: timeout if pong not sent within certain time? + connect_reactor_->EnqueueWrite( + connect_reactor_->CreateMessage(::volta::MessageType::MESSAGE_PONG)); + break; + } + + case ::volta::MessageType::MESSAGE_PONG: { + // TODO: monitor latency? + break; + } + + case ::volta::MessageType::MESSAGE_SEND_DATA: { + SendData(); + break; + } + + case ::volta::MessageType::MESSAGE_STREAM_DATA: { + StreamData(); + break; + } + + case ::volta::MessageType::MESSAGE_ERROR: { + std::cerr << "Received error message from server" << std::endl; + break; + } + + case ::volta::MessageType::MESSAGE_OK: { + break; + } + + case ::volta::MessageType::MESSAGE_ID: { + std::cout << "Received ID from server: " << msg.payload() << std::endl; + SaveUUID(msg.payload()); + break; + } + + default: { + std::cerr << "Received unknown message type from server" << std::endl; + break; + } + } +} + +void Client::SendData() { + connect_reactor_->EnqueueWrite( + connect_reactor_->CreateMessage(::volta::MessageType::MESSAGE_OK)); +} + +void Client::StreamData() { + if (buffer_ == nullptr) { + std::cerr << "Cannot stream data: buffer is null" << std::endl; + connect_reactor_->EnqueueWrite(connect_reactor_->CreateMessage( + ::volta::MessageType::MESSAGE_ERROR, "Metrics buffer is null")); + return; + } + + if (stream_data_reactor_ != nullptr) { + std::cerr << "StreamData RPC already active, refusing to start another" + << std::endl; + connect_reactor_->EnqueueWrite(connect_reactor_->CreateMessage( + ::volta::MessageType::MESSAGE_ERROR, "StreamData RPC already active")); + return; + } + + stream_data_reactor_ = std::make_unique( + stub_.get(), id_, buffer_, [this](const grpc::Status& status) { + if (!status.ok()) { + std::cerr << "StreamData RPC failed: " << status.error_message() + << std::endl; + } else { + std::cout << "StreamData RPC completed successfully" << std::endl; + } + }); + + connect_reactor_->EnqueueWrite( + connect_reactor_->CreateMessage(::volta::MessageType::MESSAGE_OK)); +} + +} // namespace client +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/client/volta_collector_client.h b/sources/agent/src/client/volta_collector_client.h new file mode 100644 index 0000000..a9dab71 --- /dev/null +++ b/sources/agent/src/client/volta_collector_client.h @@ -0,0 +1,56 @@ +#ifndef VOLTA_AGENT_CLIENT_VOLTA_COLLECTOR_CLIENT_H_ +#define VOLTA_AGENT_CLIENT_VOLTA_COLLECTOR_CLIENT_H_ + +#include +#include +#include + +#include "config/config.h" +#include "connect_reactor.h" +#include "imessage_handler.h" +#include "stream_metrics_reactor.h" +#include "volta.grpc.pb.h" + +namespace volta { +namespace agent { + +class MetricsBuffer; + +namespace client { + +class Client final : public IMessageHandler { + public: + Client(std::shared_ptr channel, config::Config& config, + std::shared_ptr<::volta::agent::MetricsBuffer> buffer); + ~Client() override = default; + + void Connect(); + void OnMessage( + const ::volta::ControlMessage& msg) override; // IMessageHandler + + static std::shared_ptr CreateChannel( + const std::string& address); + + private: + void SendData(); + void StreamData(); + + bool LoadUUID(std::string& out_uuid); + void SaveUUID(const std::string& uuid); + + std::string id_ = ""; + config::Config& config_; + std::shared_ptr<::volta::agent::MetricsBuffer> buffer_; + + std::unique_ptr<::volta::VoltaCollector::Stub> stub_; + std::unique_ptr connect_reactor_; + std::unique_ptr stream_data_reactor_; + + static std::filesystem::path kUUIDFile; +}; + +} // namespace client +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CLIENT_VOLTA_COLLECTOR_CLIENT_H_ \ No newline at end of file diff --git a/sources/agent/src/collectors/collector.h b/sources/agent/src/collectors/collector.h new file mode 100644 index 0000000..735abfb --- /dev/null +++ b/sources/agent/src/collectors/collector.h @@ -0,0 +1,82 @@ +#ifndef VOLTA_AGENT_SRC_COLLECTORS_COLLECTOR_H_ +#define VOLTA_AGENT_SRC_COLLECTORS_COLLECTOR_H_ + +#include + +#include "metric.h" +#include "platform/hardware_info.h" + +namespace volta { +namespace agent { +namespace collectors { + +class Collector { + public: + virtual ~Collector() = default; + + virtual std::vector Collect() = 0; + + virtual bool IsSupported() = 0; + + virtual std::vector Satisfiable() = 0; + + virtual void SetRequestedMetrics(const std::vector& metrics) = 0; + + virtual bool Init() { return true; } +}; + +class CollectorRegistry { + public: + static CollectorRegistry& Instance() { + static CollectorRegistry instance; + return instance; + } + + void Register(std::unique_ptr collector) { + entries_.push_back(std::move(collector)); + } + + std::vector Resolve(const std::vector& desired) { + std::vector result; + for (auto& collector : entries_) { + if (!collector->IsSupported()) continue; + + std::vector active; + for (const auto& type : collector->Satisfiable()) { + if (std::find(desired.begin(), desired.end(), type) != desired.end()) { + active.push_back(type); + } + } + + if (!active.empty()) { + collector->SetRequestedMetrics(active); + result.push_back(collector.get()); + } + } + return result; + } + + private: + CollectorRegistry() = default; + std::vector> entries_; +}; + +template +class RegisteredCollector : public Collector { + static bool Register() { + std::unique_ptr c = std::make_unique(); + CollectorRegistry::Instance().Register(std::move(c)); + return true; + } + static inline bool registered_ = Register(); + + static constexpr std::integral_constant force_{}; + + public: +}; + +} // namespace collectors +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_COLLECTORS_COLLECTOR_H_ diff --git a/sources/agent/src/collectors/nvml_collector.cc b/sources/agent/src/collectors/nvml_collector.cc new file mode 100644 index 0000000..8a301de --- /dev/null +++ b/sources/agent/src/collectors/nvml_collector.cc @@ -0,0 +1,165 @@ +#include "collectors/nvml_collector.h" + +#include +#include +#include + +namespace volta { +namespace agent { +namespace collectors { + +NvmlCollector::NvmlCollector() = default; + +NvmlCollector::~NvmlCollector() { + if (initialized_) { + nvml_->Shutdown(); + } +} + +bool NvmlCollector::Init() { + const platform::NvmlApi* nvml = platform::TryLoadNvml(); + if (nvml == nullptr) { + std::cerr << "Failed to load NVML: " << platform::NvmlLoadError() + << std::endl; + return false; + } + + nvmlReturn_t result = nvml->Init(); + if (result != NVML_SUCCESS) { + std::cerr << "Failed to initialize NVML: " << nvml->ErrorString(result) + << std::endl; + return false; + } + + result = nvml->DeviceGetHandleByIndex(0, &device_handle_); + if (result != NVML_SUCCESS) { + std::cerr << "Failed to get device handle: " << nvml->ErrorString(result) + << std::endl; + nvml->Shutdown(); + return false; + } + + nvmlPciInfo_t pci_info; + result = nvml->DeviceGetPciInfo(device_handle_, &pci_info); + if (result != NVML_SUCCESS) { + std::cerr << "Failed to get PCI info: " << nvml->ErrorString(result) + << std::endl; + nvml->Shutdown(); + return false; + } + + GpuID gpu_id; + gpu_id.set_pci_domain(pci_info.domain); + gpu_id.set_pci_bus(pci_info.bus); + gpu_id.set_pci_device(pci_info.device); + gpu_id.set_pci_function(0); + gpu_id_ = std::move(gpu_id); + + nvml_ = nvml; + initialized_ = true; + return true; +} + +bool NvmlCollector::IsSupported() { + const platform::NvmlApi* nvml = platform::TryLoadNvml(); + if (nvml == nullptr) { + return false; + } + + nvmlReturn_t result = nvml->Init(); + if (result != NVML_SUCCESS) { + return false; + } + + unsigned int device_count = 0; + result = nvml->DeviceGetCount(&device_count); + if (result != NVML_SUCCESS || device_count == 0) { + nvml->Shutdown(); + return false; + } + + nvmlDevice_t device; + result = nvml->DeviceGetHandleByIndex(0, &device); + nvml->Shutdown(); + return result == NVML_SUCCESS; +} + +void NvmlCollector::SetRequestedMetrics( + const std::vector& metrics) { + requested_metrics_ = metrics; +} + +std::vector NvmlCollector::Collect() { + if (!initialized_ || requested_metrics_.empty()) return {}; + + const auto& nvml = *nvml_; + std::vector metrics; + unsigned int power_mw = 0; + auto now = std::chrono::system_clock::now().time_since_epoch().count(); + + auto needs = [&](MetricType type) { + return std::find(requested_metrics_.begin(), requested_metrics_.end(), + type) != requested_metrics_.end(); + }; + + nvmlReturn_t result; + if (needs(MetricType::METRIC_TYPE_GPU_POWER)) { + result = nvml.DeviceGetPowerUsage(device_handle_, &power_mw); + if (result == NVML_SUCCESS) { + metrics.push_back({MetricType::METRIC_TYPE_GPU_POWER, gpu_id_, + static_cast(power_mw) / 1000.0, now}); + } + } + + if (needs(MetricType::METRIC_TYPE_GPU_TEMPERATURE)) { + unsigned int temp_c = 0; + result = nvml.DeviceGetTemperature(device_handle_, NVML_TEMPERATURE_GPU, + &temp_c); + if (result == NVML_SUCCESS) { + metrics.push_back({MetricType::METRIC_TYPE_GPU_TEMPERATURE, gpu_id_, + static_cast(temp_c), now}); + } + } + + if (needs(MetricType::METRIC_TYPE_GPU_UTILIZATION) || + needs(MetricType::METRIC_TYPE_GPU_SHARED_MEMORY_UTILIZATION)) { + nvmlUtilization_t utilization; + result = nvml.DeviceGetUtilizationRates(device_handle_, &utilization); + if (result == NVML_SUCCESS) { + if (needs(MetricType::METRIC_TYPE_GPU_UTILIZATION)) { + metrics.push_back({MetricType::METRIC_TYPE_GPU_UTILIZATION, gpu_id_, + static_cast(utilization.gpu), now}); + } + if (needs(MetricType::METRIC_TYPE_GPU_SHARED_MEMORY_UTILIZATION)) { + metrics.push_back( + {MetricType::METRIC_TYPE_GPU_SHARED_MEMORY_UTILIZATION, gpu_id_, + static_cast(utilization.memory), now}); + } + } + } + + if (needs(MetricType::METRIC_TYPE_GPU_VRAM_USED)) { + nvmlMemory_t memory; + result = nvml.DeviceGetMemoryInfo(device_handle_, &memory); + if (result == NVML_SUCCESS) { + metrics.push_back( + {MetricType::METRIC_TYPE_GPU_VRAM_USED, gpu_id_, + static_cast(memory.used) / static_cast(memory.total), + now}); + } + } + + return metrics; +} + +std::vector NvmlCollector::Satisfiable() { + return {MetricType::METRIC_TYPE_GPU_VRAM_USED, + MetricType::METRIC_TYPE_GPU_UTILIZATION, + MetricType::METRIC_TYPE_GPU_SHARED_MEMORY_UTILIZATION, + MetricType::METRIC_TYPE_GPU_TEMPERATURE, + MetricType::METRIC_TYPE_GPU_POWER}; +} + +} // namespace collectors +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/collectors/nvml_collector.h b/sources/agent/src/collectors/nvml_collector.h new file mode 100644 index 0000000..b0660c9 --- /dev/null +++ b/sources/agent/src/collectors/nvml_collector.h @@ -0,0 +1,40 @@ +#ifndef VOLTA_AGENT_SRC_COLLECTORS_NVML_COLLECTOR_H_ +#define VOLTA_AGENT_SRC_COLLECTORS_NVML_COLLECTOR_H_ + +#include + +#include "collectors/collector.h" +#include "platform/nvml_loader.h" + +namespace volta { +namespace agent { +namespace collectors { + +class NvmlCollector final : public RegisteredCollector { + public: + NvmlCollector(); + ~NvmlCollector() override; + + NvmlCollector(NvmlCollector&&) = default; + NvmlCollector& operator=(NvmlCollector&&) = default; + + bool Init() override; + std::vector Collect() override; + bool IsSupported() override; + std::vector Satisfiable() override; + void SetRequestedMetrics(const std::vector& metrics) override; + + private: + std::vector requested_metrics_; + std::optional gpu_id_; + + const platform::NvmlApi* nvml_ = nullptr; + nvmlDevice_t device_handle_ = nullptr; + bool initialized_ = false; +}; + +} // namespace collectors +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_COLLECTORS_NVML_COLLECTOR_H_ diff --git a/sources/agent/src/collectors/proc_stat_collector.cc b/sources/agent/src/collectors/proc_stat_collector.cc new file mode 100644 index 0000000..d774f10 --- /dev/null +++ b/sources/agent/src/collectors/proc_stat_collector.cc @@ -0,0 +1,80 @@ +#include "collectors/proc_stat_collector.h" + +#include +#include +#include +#include + +namespace volta { +namespace agent { +namespace collectors { + +bool ProcStatCollector::Init() { return true; } + +void ProcStatCollector::SetRequestedMetrics( + const std::vector& metrics) { + requested_metrics_ = metrics; +} + +std::vector ProcStatCollector::Collect() { + if (requested_metrics_.empty()) return {}; + + if (std::find(requested_metrics_.begin(), requested_metrics_.end(), + MetricType::METRIC_TYPE_CPU_UTILIZATION) == + requested_metrics_.end()) { + return {}; + } + + uint64_t current_total = 0; + uint64_t current_idle = 0; + + ReadCpuStats(current_total, current_idle); + + uint64_t diff_total = current_total - prev_total_; + uint64_t diff_idle = current_idle - prev_idle_; + + double usage_percent = 0.0; + if (diff_total > 0) { + usage_percent = (double)(diff_total - diff_idle) / diff_total * 100.0; + } + + prev_total_ = current_total; + prev_idle_ = current_idle; + + Metric m; + m.type = MetricType::METRIC_TYPE_CPU_UTILIZATION; + m.devId = std::nullopt; + m.value = usage_percent; + m.timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + + return {m}; +} + +bool ProcStatCollector::IsSupported() { + return std::filesystem::exists("/proc/stat"); +} + +std::vector ProcStatCollector::Satisfiable() { + return {MetricType::METRIC_TYPE_CPU_UTILIZATION}; +} + +void ProcStatCollector::ReadCpuStats(uint64_t& total, uint64_t& idle) { + std::ifstream file("/proc/stat"); + std::string line; + + if (std::getline(file, line)) { + std::istringstream iss(line); + std::string cpu_label; + uint64_t user, nice, system, cur_idle, iowait, irq, softirq, steal; + + iss >> cpu_label >> user >> nice >> system >> cur_idle >> iowait >> irq >> + softirq >> steal; + + idle = cur_idle + iowait; + total = user + nice + system + idle + irq + softirq + steal; + } +} + +} // namespace collectors +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/collectors/proc_stat_collector.h b/sources/agent/src/collectors/proc_stat_collector.h new file mode 100644 index 0000000..9e0a956 --- /dev/null +++ b/sources/agent/src/collectors/proc_stat_collector.h @@ -0,0 +1,32 @@ +#ifndef VOLTA_AGENT_SRC_COLLECTORS_PROC_STAT_COLLECTOR_H_ +#define VOLTA_AGENT_SRC_COLLECTORS_PROC_STAT_COLLECTOR_H_ + +#include + +#include "collectors/collector.h" + +namespace volta { +namespace agent { +namespace collectors { + +class ProcStatCollector final : public RegisteredCollector { + public: + bool Init() override; + std::vector Collect() override; + bool IsSupported() override; + std::vector Satisfiable() override; + void SetRequestedMetrics(const std::vector& metrics) override; + + private: + void ReadCpuStats(uint64_t& idle_time, uint64_t& total_time); + + std::vector requested_metrics_; + uint64_t prev_total_ = 0; + uint64_t prev_idle_ = 0; +}; + +} // namespace collectors +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_COLLECTORS_PROC_STAT_COLLECTOR_H_ diff --git a/sources/agent/src/collectors/ram_collector.cc b/sources/agent/src/collectors/ram_collector.cc new file mode 100644 index 0000000..4abb101 --- /dev/null +++ b/sources/agent/src/collectors/ram_collector.cc @@ -0,0 +1,81 @@ +#include "collectors/ram_collector.h" + +#include +#include +#include +#include +#include + +namespace volta { +namespace agent { +namespace collectors { + +bool RamCollector::Init() { + initialized_ = true; + return true; +} + +void RamCollector::SetRequestedMetrics(const std::vector& metrics) { + requested_metrics_ = metrics; +} + +std::vector RamCollector::Collect() { + if (!initialized_ || requested_metrics_.empty()) return {}; + + bool needs_total = + std::find(requested_metrics_.begin(), requested_metrics_.end(), + MetricType::METRIC_TYPE_RAM_TOTAL) != requested_metrics_.end(); + bool needs_used = + std::find(requested_metrics_.begin(), requested_metrics_.end(), + MetricType::METRIC_TYPE_RAM_USED) != requested_metrics_.end(); + if (!needs_total && !needs_used) return {}; + + uint64_t used = 0; + uint64_t total = 0; + ReadStats(used, total); + + auto now = std::chrono::system_clock::now().time_since_epoch().count(); + std::vector metrics; + if (needs_total) { + metrics.push_back( + {MetricType::METRIC_TYPE_RAM_TOTAL, std::nullopt, (double)total, now}); + } + if (needs_used) { + metrics.push_back( + {MetricType::METRIC_TYPE_RAM_USED, std::nullopt, (double)used, now}); + } + return metrics; +} + +std::vector RamCollector::Satisfiable() { + return {MetricType::METRIC_TYPE_RAM_TOTAL, MetricType::METRIC_TYPE_RAM_USED}; +} + +void RamCollector::ReadStats(uint64_t& used, uint64_t& total) { + std::ifstream file("/proc/meminfo"); + std::string line, key; + uint64_t value; + std::string unit; + uint64_t available = 0; + + while (std::getline(file, line)) { + std::istringstream iss(line); + iss >> key >> value >> unit; + if (key == "MemTotal:") { + total = value * 1024; + } else if (key == "MemAvailable:") { + available = value * 1024; + } + if (total > 0 && available > 0) break; + } + + used = total - available; +} + +bool RamCollector::IsSupported() { + return std::filesystem::exists("/proc/meminfo"); +} + +} // namespace collectors +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/collectors/ram_collector.h b/sources/agent/src/collectors/ram_collector.h new file mode 100644 index 0000000..a4dca7b --- /dev/null +++ b/sources/agent/src/collectors/ram_collector.h @@ -0,0 +1,29 @@ +#ifndef VOLTA_AGENT_SRC_COLLECTORS_RAM_COLLECTOR_H_ +#define VOLTA_AGENT_SRC_COLLECTORS_RAM_COLLECTOR_H_ + +#include "collectors/collector.h" + +namespace volta { +namespace agent { +namespace collectors { + +class RamCollector final : public RegisteredCollector { + public: + bool Init() override; + std::vector Collect() override; + bool IsSupported() override; + std::vector Satisfiable() override; + void SetRequestedMetrics(const std::vector& metrics) override; + + private: + void ReadStats(uint64_t& used, uint64_t& total); + + std::vector requested_metrics_; + bool initialized_ = false; +}; + +} // namespace collectors +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_COLLECTORS_RAM_COLLECTOR_H_ diff --git a/sources/agent/src/collectors/rapl_collector.cc b/sources/agent/src/collectors/rapl_collector.cc new file mode 100644 index 0000000..2f8c64a --- /dev/null +++ b/sources/agent/src/collectors/rapl_collector.cc @@ -0,0 +1,142 @@ +#include "rapl_collector.h" + +#include +#include + +#include +#include +#include +#include + +namespace volta { +namespace agent { +namespace collectors { + +RaplCollector::RaplCollector() = default; + +bool RaplCollector::Init() { + try { + OpenMSR(); + uint64_t readout = ReadMSR(0, MSR_RAPL::POWER_UNIT); + power_units_ = pow(0.5, (double)(readout & 0xf)); + energy_units_ = pow(0.5, (double)((readout >> 8) & 0x1f)); + time_units_ = pow(0.5, (double)((readout >> 16) & 0xf)); + readout = ReadMSR(0, MSR_RAPL::PKG::ENERGY_STATUS); + last_value = energy_units_ * readout; + initialized_ = true; + return true; + } catch (const MSR_Open_Exception&) { + return false; + } catch (const MSR_Read_Exception&) { + return false; + } +} + +bool RaplCollector::IsSupported() { + const std::filesystem::path cpu_base = "/dev/cpu"; + std::error_code ec; + if (!std::filesystem::exists(cpu_base, ec) || + !std::filesystem::is_directory(cpu_base, ec)) { + return false; + } + + for (const auto& entry : std::filesystem::directory_iterator(cpu_base)) { + if (!entry.is_directory()) continue; + + const auto msr_path = entry.path() / "msr"; + if (std::filesystem::exists(msr_path, ec) && + access(msr_path.c_str(), R_OK) == 0) { + return true; + } + } + + return false; +} + +void RaplCollector::SetRequestedMetrics( + const std::vector& metrics) { + requested_metrics_ = metrics; +} + +std::vector RaplCollector::Collect() { + if (!initialized_ || requested_metrics_.empty()) return {}; + if (std::find(requested_metrics_.begin(), requested_metrics_.end(), + MetricType::METRIC_TYPE_CPU_POWER_PACKAGE) == + requested_metrics_.end()) { + return {}; + } + + uint64_t readout; + try { + readout = ReadMSR(0, MSR_RAPL::PKG::ENERGY_STATUS); + } catch (const MSR_Read_Exception&) { + return {}; + } + + double value = energy_units_ * readout; + Metric m; + m.type = MetricType::METRIC_TYPE_CPU_POWER_PACKAGE; + CpuID cpu_id; + cpu_id.set_socket_index(0); + cpu_id.set_core_index(0); + m.devId = DeviceId{std::move(cpu_id)}; + m.value = value - last_value; + m.timestamp = std::chrono::system_clock::now().time_since_epoch().count(); + last_value = value; + return {m}; +} + +std::vector RaplCollector::Satisfiable() { + return {MetricType::METRIC_TYPE_CPU_POWER_PACKAGE}; +} + +uint64_t RaplCollector::ReadMSR(uint8_t core, uint32_t offset) { + uint64_t data; + if (core + 1 > MSR_files_.size()) { + throw MSR_Read_Exception(); + } + if (pread(MSR_files_[core], &data, sizeof data, offset) != sizeof data) { + throw MSR_Read_Exception(); + } + return data; +} + +void RaplCollector::OpenMSR() { + const std::filesystem::path cpu_base = "/dev/cpu"; + MSR_files_.clear(); + std::error_code ec; + + if (!std::filesystem::exists(cpu_base, ec)) { + throw MSR_Open_Exception(); + } + std::vector> cpu_entries; + for (const auto& entry : std::filesystem::directory_iterator(cpu_base)) { + if (!entry.is_directory()) continue; + const auto& dirname = entry.path().filename().string(); + if (!std::ranges::all_of(dirname, ::isdigit)) continue; + cpu_entries.emplace_back(std::stoi(dirname), entry.path()); + } + + std::ranges::sort(cpu_entries); + for (const auto& [id, path] : cpu_entries) { + int fd = open((path / "msr").c_str(), O_RDONLY); + if (fd >= 0) { + MSR_files_.push_back(fd); + } + } + if (MSR_files_.empty()) { + throw MSR_Open_Exception(); + } +} + +void RaplCollector::CloseMSR(int fd) { close(fd); } + +RaplCollector::~RaplCollector() { + for (auto file : MSR_files_) { + CloseMSR(file); + } +} + +} // namespace collectors +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/collectors/rapl_collector.h b/sources/agent/src/collectors/rapl_collector.h new file mode 100644 index 0000000..9e9817c --- /dev/null +++ b/sources/agent/src/collectors/rapl_collector.h @@ -0,0 +1,78 @@ +#ifndef VOLTA_AGENT_SRC_COLLECTORS_RAPL_COLLECTOR_H_ +#define VOLTA_AGENT_SRC_COLLECTORS_RAPL_COLLECTOR_H_ + +#include "collectors/collector.h" + +namespace volta { +namespace agent { +namespace collectors { + +class RaplCollector final : public RegisteredCollector { + public: + RaplCollector(); + RaplCollector(const RaplCollector&) = delete; + RaplCollector& operator=(const RaplCollector&) = delete; + bool Init() override; + std::vector Collect() override; + bool IsSupported() override; + std::vector Satisfiable() override; + void SetRequestedMetrics(const std::vector& metrics) override; + ~RaplCollector(); + + private: + uint64_t ReadMSR(uint8_t core, uint32_t offset); + void OpenMSR(); + void CloseMSR(int fd); + std::vector requested_metrics_; + bool initialized_ = false; + double power_units_ = 0, energy_units_ = 0, time_units_ = 0; + std::vector MSR_files_; + double last_value = 0; + + class MSR_Read_Exception : std::exception {}; + class MSR_Open_Exception : std::exception {}; + + struct MSR_RAPL { + static constexpr uint32_t POWER_UNIT = 0x606; + struct Units { + static constexpr uint32_t POWER_UNIT_OFFSET = 0; + static constexpr uint32_t POWER_UNIT_MASK = 0x0F; + static constexpr uint32_t ENERGY_UNIT_OFFSET = 0x08; + static constexpr uint32_t ENERGY_UNIT_MASK = 0x1F00; + static constexpr uint32_t TIME_UNIT_OFFSET = 0x10; + static constexpr uint32_t TIME_UNIT_MASK = 0xF000; + }; + + struct PKG { + static constexpr uint32_t POWER_LIMIT = 0x610; + static constexpr uint32_t ENERGY_STATUS = 0x611; + static constexpr uint32_t PERF_STATUS = 0x613; + static constexpr uint32_t POWER_INFO = 0x614; + }; + + struct PP0 { + static constexpr uint32_t POWER_LIMIT = 0x638; + static constexpr uint32_t ENERGY_STATUS = 0x639; + static constexpr uint32_t POLICY = 0x63A; + static constexpr uint32_t PERF_STATUS = 0x63B; + }; + + struct PP1 { + static constexpr uint32_t POWER_LIMIT = 0x640; + static constexpr uint32_t ENERGY_STATUS = 0x641; + static constexpr uint32_t POLICY = 0x642; + }; + + struct DRAM { + static constexpr uint32_t POWER_LIMIT = 0x618; + static constexpr uint32_t ENERGY_STATUS = 0x619; + static constexpr uint32_t PERF_STATUS = 0x61B; + static constexpr uint32_t POWER_INFO = 0x61C; + }; + }; +}; + +} // namespace collectors +} // namespace agent +} // namespace volta +#endif diff --git a/sources/agent/src/config/config.h b/sources/agent/src/config/config.h new file mode 100644 index 0000000..62e6667 --- /dev/null +++ b/sources/agent/src/config/config.h @@ -0,0 +1,71 @@ +#ifndef VOLTA_AGENT_CONFIG_CONFIG_H_ +#define VOLTA_AGENT_CONFIG_CONFIG_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace volta { +namespace agent { +namespace config { + +struct Config { + void PrintCurrentAffinity() { + cpu_set_t set; + CPU_ZERO(&set); + + if (sched_getaffinity(0, sizeof(set), &set) != 0) { + // TODO: Log + perror("sched_getaffinity"); + return; + } + + long max_cpus = sysconf(_SC_NPROCESSORS_CONF); + std::cout << "Current CPU affinity: "; + + for (int i = 0; i < max_cpus; ++i) { + if (CPU_ISSET(i, &set)) std::cout << i << " "; + } + std::cout << "\n"; + } + // TODO: move this initialization logic to the ConfigLoader's + // LoadDefaultConfig method + static constexpr int32_t kDefaultIntervalMs = 500; + static constexpr int32_t kDefaultTimeWindowMs = 2000; + static constexpr char const* kDefaultServerAddress = "localhost"; + static constexpr const char* kDefaultDumpPath = "/var/lib/volta/dumps"; + static constexpr uint16_t kDefaultServerPort = 5000; + static inline cpu_set_t kDefaultAffinity = [] { + cpu_set_t mask; + CPU_ZERO(&mask); + long n_cpus = sysconf(_SC_NPROCESSORS_ONLN); + n_cpus = std::min(n_cpus, static_cast(CPU_SETSIZE)); + // TODO: Handle sysconf error + for (long i = 0; i < n_cpus; ++i) { + CPU_SET(i, &mask); + } + return mask; + }(); + + std::chrono::milliseconds collection_interval = + std::chrono::milliseconds(kDefaultIntervalMs); + cpu_set_t core_affinity = kDefaultAffinity; + std::string server_address = kDefaultServerAddress; + uint16_t server_port = kDefaultServerPort; + std::vector requestedMetrics; + std::chrono::milliseconds buffered_time_window = + std::chrono::milliseconds(kDefaultTimeWindowMs); + std::filesystem::path dump_dir = kDefaultDumpPath; +}; + +} // namespace config +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CONFIG_CONFIG_H_ diff --git a/sources/agent/src/config/config_loader.cc b/sources/agent/src/config/config_loader.cc new file mode 100644 index 0000000..d02680c --- /dev/null +++ b/sources/agent/src/config/config_loader.cc @@ -0,0 +1,317 @@ +#include "config/config_loader.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "config_loader.h" +#include "utils/utils.h" + +namespace volta { +namespace agent { +namespace config { + +// TODO: different path for prod build +std::filesystem::path ConfigLoader::kConfigFile = "agent.conf"; + +std::set> ConfigLoader::kValidTopLevelKeys = { + "core_affinity", "interval", "server_address", + "server_port", "metrics", "time_window"}; + +Config ConfigLoader::LoadConfig() { + Config config = LoadDefaultConfig(); + LoadConfigFile(config); + return config; +} + +Config ConfigLoader::LoadDefaultConfig() { + Config config; + + // request all metrics by default for now + config.requestedMetrics = { + MetricType::METRIC_TYPE_UNSPECIFIED, + MetricType::METRIC_TYPE_CPU_POWER_PACKAGE, + MetricType::METRIC_TYPE_CPU_POWER_CORES, + MetricType::METRIC_TYPE_CPU_CLOCK_SPEED, + MetricType::METRIC_TYPE_CPU_UTILIZATION, + MetricType::METRIC_TYPE_CPU_TEMPERATURE, + MetricType::METRIC_TYPE_CPU_IOWAIT, + MetricType::METRIC_TYPE_CPU_CACHE_HIT_RATIO, + MetricType::METRIC_TYPE_CPU_ACTIVE_PROCESSES, + MetricType::METRIC_TYPE_GPU_POWER, + MetricType::METRIC_TYPE_GPU_CLOCK_SPEED, + MetricType::METRIC_TYPE_GPU_UTILIZATION, + MetricType::METRIC_TYPE_GPU_TEMPERATURE, + MetricType::METRIC_TYPE_GPU_VRAM_USED, + MetricType::METRIC_TYPE_GPU_PCIE_BANDWIDTH, + MetricType::METRIC_TYPE_GPU_COMPUTE_UNIT_UTILIZATION, + MetricType::METRIC_TYPE_GPU_SHARED_MEMORY_UTILIZATION, + MetricType::METRIC_TYPE_GPU_REGISTER_UTILIZATION, + MetricType::METRIC_TYPE_RAM_POWER, + MetricType::METRIC_TYPE_RAM_TOTAL, + MetricType::METRIC_TYPE_RAM_AVAILABLE, + MetricType::METRIC_TYPE_RAM_USED, + MetricType::METRIC_TYPE_RAM_CACHED, + MetricType::METRIC_TYPE_SWAP_USED, + MetricType::METRIC_TYPE_SWAP_ACTIVITY, + MetricType::METRIC_TYPE_DISK_READ_THROUGHPUT, + MetricType::METRIC_TYPE_DISK_WRITE_THROUGHPUT, + MetricType::METRIC_TYPE_DISK_READ_IOPS, + MetricType::METRIC_TYPE_DISK_WRITE_IOPS, + MetricType::METRIC_TYPE_DISK_BUSY_TIME, + MetricType::METRIC_TYPE_DISK_CAPACITY_USED, + MetricType::METRIC_TYPE_NET_BYTES_RECEIVED, + MetricType::METRIC_TYPE_NET_BYTES_SENT, + MetricType::METRIC_TYPE_NET_PACKETS_RECEIVED, + MetricType::METRIC_TYPE_NET_PACKETS_SENT, + }; + + return config; +} + +inline unsigned int MaxOnlineCpus() { + long n = sysconf(_SC_NPROCESSORS_ONLN); + return (n > 0) ? static_cast(n) : 0; +} + +bool AddCpu(cpu_set_t& set, unsigned int cpu, unsigned int max_cpu) { + if (cpu >= max_cpu) return false; + CPU_SET(cpu, &set); + return true; +} + +bool AddRange(cpu_set_t& set, unsigned int from, unsigned int to, + unsigned int max_cpu) { + if (from > to || to >= max_cpu) return false; + for (unsigned int i = from; i <= to; ++i) CPU_SET(i, &set); + return true; +} + +void ConfigLoader::LoadConfigFile(Config& out_config) { + // TODO: Proper logging + if (!std::filesystem::exists(kConfigFile)) { + std::cout << "Agent config file not found, loading default settings." + << std::endl; + return; + } + + try { + toml::table tbl = toml::parse_file(kConfigFile.string()); + + LoadCoreAffinity(tbl, out_config); + LoadInterval(tbl, out_config); + LoadTimeWindow(tbl, out_config); + LoadServerAddress(tbl, out_config); + LoadServerPort(tbl, out_config); + LoadMetrics(tbl, out_config); + CheckKeys(tbl); + } catch (const toml::parse_error& err) { + std::cerr << "Parsing Agent config failed: " << err.description() << " at " + << err.source().begin << std::endl; + } +} + +void ConfigLoader::LoadCoreAffinity(toml::table& tbl, Config& out_config) { + if (!tbl.contains("core_affinity")) return; + + auto val = tbl["core_affinity"]; + + // core_affinity = "all" + if (auto s = val.value(); s && *s == "all") { + out_config.core_affinity = Config::kDefaultAffinity; + } + // core_affinity = [ ... ] + else if (auto arr = val.as_array()) { + unsigned int max_cpu = MaxOnlineCpus(); + cpu_set_t mask; + CPU_ZERO(&mask); + // NOTE: Should setting the affinity in here? + for (auto& item : *arr) { + // liczba CPU + if (auto cpu = item.value()) { + if (!AddCpu(mask, *cpu, max_cpu)) { + std::cerr << "CPU index out of range: " << *cpu << "\n"; + return; + } + } + // zakres "X-Y" + else if (auto str = item.value()) { + unsigned int from, to; + if (sscanf(str->c_str(), "%u-%u", &from, &to) == 2) { + if (!AddRange(mask, from, to, max_cpu)) { + std::cerr << "Invalid CPU range: " << *str << "\n"; + return; + } + } else { + std::cerr << "Invalid core_affinity entry: " << *str << "\n"; + return; + } + } else { + std::cerr << "Invalid core_affinity element type\n"; + return; + } + } + if (sched_setaffinity(0, sizeof(cpu_set_t), &mask) == -1) { + perror("sched_setaffinity"); + } else { + std::cout << "Successfully set CPU affinity mask." << std::endl; + } + + out_config.core_affinity = mask; + } else { + std::cerr << "Invalid core_affinity value\n"; + } +} + +void ConfigLoader::LoadInterval(toml::table& tbl, Config& out_config) { + if (!tbl.contains("interval")) return; + + auto val = tbl["interval"]; + + if (auto ms = val.value()) { + out_config.collection_interval = std::chrono::milliseconds(*ms); + std::cout << "Collection Interval set to: " + << out_config.collection_interval << std::endl; + } else { + std::cerr << "Invalid interval value type, use uint32" << std::endl; + } +} + +void ConfigLoader::LoadServerAddress(toml::table& tbl, Config& out_config) { + if (!tbl.contains("server_address")) return; + + auto val = tbl["server_address"]; + + if (auto str = val.value()) { + if (utils::IsValidIP(*str) || utils::IsResolvable(*str)) { + out_config.server_address = *str; + std::cout << "Server Address set to " << *str << std::endl; + } else { + std::cerr << "Invalid server_address format" << std::endl; + } + } else { + std::cerr << "Invalid server_address value type, use string" << std::endl; + } +} + +void ConfigLoader::LoadServerPort(toml::table& tbl, Config& out_config) { + if (!tbl.contains("server_port")) return; + + auto val = tbl["server_port"]; + + if (auto port = val.value(); port && *port > 0) { + out_config.server_port = *port; + std::cout << "Server port set to " << *port << std::endl; + } else { + std::cerr << "server_port has an incorrect type or value, use number " + "from range " + "[1, 65535]" + << std::endl; + } +} + +void ConfigLoader::LoadMetrics(toml::table& tbl, Config& out_config) { + if (!tbl.contains("metrics")) return; + + auto metrics_node = tbl["metrics"]; + + std::vector metrics; + + auto append_metric = [&](MetricType metric) { + if (metric == MetricType::METRIC_TYPE_UNSPECIFIED) { + return; + } + if (std::find(metrics.begin(), metrics.end(), metric) == metrics.end()) { + metrics.push_back(metric); + } + }; + + auto parse_metric_name = [&](const std::string& name, + MetricType& metric) -> bool { + if (MetricType_Parse(name, &metric)) return true; + if (MetricType_Parse(std::string("METRIC_TYPE_") + name, &metric)) + return true; + return false; + }; + + if (auto arr = metrics_node.as_array()) { + for (const auto& item : *arr) { + if (auto metric_num = item.value()) { + if (!MetricType_IsValid(*metric_num)) { + std::cerr << "Invalid metric value: " << *metric_num << std::endl; + continue; + } + append_metric(static_cast(*metric_num)); + continue; + } + + if (auto metric_name = item.value()) { + MetricType metric; + if (!parse_metric_name(*metric_name, metric)) { + std::cerr << "Invalid metric name: " << *metric_name << std::endl; + continue; + } + append_metric(metric); + continue; + } + + std::cerr << "Invalid metric entry type" << std::endl; + } + } else if (auto val = metrics_node.value()) { + MetricType metric; + if (parse_metric_name(*val, metric)) { + append_metric(metric); + } else { + std::cerr << "Invalid metric name: " << *val << std::endl; + } + } else { + std::cerr << "Invalid metrics value, use array of strings or numbers" + << std::endl; + return; + } + + if (!metrics.empty()) { + out_config.requestedMetrics = std::move(metrics); + std::cout << "Requested metrics updated to " + << out_config.requestedMetrics.size() << " entries" << std::endl; + } +} + +void ConfigLoader::LoadTimeWindow(toml::table& tbl, Config& out_config) { + if (!tbl.contains("time_window")) { + return; + } + + auto val = tbl["time_window"]; + + if (auto window = val.value(); + window && + (*window * 1000) > (float)out_config.collection_interval.count()) { + out_config.buffered_time_window = + std::chrono::milliseconds((uint32_t)(*window * 1000)); + std::cout << "Time window set to " << *window << std::endl; + } else { + std::cerr << "time_window has an incorrect type or value, use number " + "larger than collection interval (" + << (float_t)out_config.collection_interval.count() / 1000.0 + << "s)" << std::endl; + } +} + +void ConfigLoader::CheckKeys(toml::table& tbl) { + for (auto&& [key, value] : tbl) { + if (!kValidTopLevelKeys.contains(key.str())) { + std::cout << "Key '" << key << "' is not a valid key\n"; + } + } +} + +} // namespace config +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/config/config_loader.h b/sources/agent/src/config/config_loader.h new file mode 100644 index 0000000..73b4cec --- /dev/null +++ b/sources/agent/src/config/config_loader.h @@ -0,0 +1,46 @@ +#ifndef VOLTA_AGENT_CONFIG_CONFIG_LOADER_H_ +#define VOLTA_AGENT_CONFIG_CONFIG_LOADER_H_ + +#include +#include +#include +#include + +#include "config/config.h" + +namespace volta { +namespace agent { +namespace config { + +class ConfigLoader { + public: + static Config LoadConfig(); + static Config LoadDefaultConfig(); + + private: + ConfigLoader() = delete; + ConfigLoader(const ConfigLoader&) = delete; + ConfigLoader(ConfigLoader&&) = delete; + void operator=(const ConfigLoader&) = delete; + void operator=(ConfigLoader&&) = delete; + + static void LoadConfigFile(Config& out_config); + static void CreateUUID(Config& out_config); + static void LoadCoreAffinity(toml::table& tbl, Config& out_config); + static void LoadInterval(toml::table& tbl, Config& out_config); + static void LoadServerAddress(toml::table& tbl, Config& out_config); + static void LoadServerPort(toml::table& tbl, Config& out_config); + static void LoadMetrics(toml::table& tbl, Config& out_config); + static void LoadTimeWindow(toml::table& tbl, Config& out_config); + static void CheckKeys(toml::table& tbl); + + static std::filesystem::path kConfigFile; + static std::filesystem::path kUUIDFile; + static std::set> kValidTopLevelKeys; +}; + +} // namespace config +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_CONFIG_CONFIG_LOADER_H_ diff --git a/sources/agent/src/exporter.cc b/sources/agent/src/exporter.cc new file mode 100644 index 0000000..5557e4a --- /dev/null +++ b/sources/agent/src/exporter.cc @@ -0,0 +1,71 @@ +#include "exporter.h" + +#include + +namespace volta { +namespace agent { + +Exporter::Exporter(const config::Config& cfg) : dump_file_() { + if (std::filesystem::exists(cfg.dump_dir)) { + dump_dir_ = cfg.dump_dir; + } +} + +void Exporter::Dump(const std::vector& metrics) { + if (!IsActive()) { + return; + } + + for (auto metric : metrics) + dump_file_ << MetricType_Name(metric.type) << ";" << DescribeDevice(metric) + << ";" << metric.timestamp << ";" << metric.value << "\n"; +} + +void Exporter::StartDump( + std::optional dump_dir_overload) { + if (is_exporting) return; + + auto dir = dump_dir_overload.has_value() ? *dump_dir_overload : dump_dir_; + if (!std::filesystem::exists(dir)) return; + + dump_file_.open(dir.string() + std::format("/dump_{}.csv", + std::chrono::system_clock::now()), + std::ios_base::out | std::ios_base::trunc); + + dump_file_ << "metric_type;device_id;timestamp;value\n"; + is_exporting = true; +} + +void Exporter::EndDump() { + if (dump_file_.is_open()) dump_file_.close(); + is_exporting = false; +} + +std::string Exporter::DescribeDevice(const Metric& m) { + std::ostringstream out; + if (m.devId.has_value()) + std::visit( + [&](const auto& id) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + out << "gpu=" << id.pci_domain() << ':' + << static_cast(id.pci_bus()) << ':' + << static_cast(id.pci_device()) << '.' + << static_cast(id.pci_function()); + } else if constexpr (std::is_same_v) { + out << "cpu=" << static_cast(id.socket_index()) << "/" + << id.core_index(); + } else if constexpr (std::is_same_v) { + out << "net_ifindex=" << id.ifindex(); + } else if constexpr (std::is_same_v) { + out << "disk=" << id.major() << ':' << id.minor(); + } + }, + *m.devId); + else + out << "system"; + return out.str(); +} + +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/exporter.h b/sources/agent/src/exporter.h new file mode 100644 index 0000000..4f277ed --- /dev/null +++ b/sources/agent/src/exporter.h @@ -0,0 +1,32 @@ +#ifndef VOLTA_AGENT_SRC_DUMP_H_ +#define VOLTA_AGENT_SRC_DUMP_H_ + +#include + +#include "config/config.h" +#include "metric.h" + +namespace volta { +namespace agent { + +class Exporter { + public: + explicit Exporter(const config::Config& cfg); + void Dump(const std::vector& metric); + void StartDump(std::optional dump_dir_overload); + void EndDump(); + bool IsActive() const { return is_exporting; } + + private: + std::string DescribeDevice(const Metric& m); + + std::ofstream dump_file_; + std::filesystem::path dump_dir_; + std::chrono::milliseconds dump_start; + bool is_exporting = false; +}; + +} // namespace agent +} // namespace volta + +#endif diff --git a/sources/agent/src/main.cc b/sources/agent/src/main.cc new file mode 100644 index 0000000..4f3ae5c --- /dev/null +++ b/sources/agent/src/main.cc @@ -0,0 +1,103 @@ +#include +#include + +#include "client/volta_collector_client.h" +#include "collectors/collector.h" +#include "config/config.h" +#include "config/config_loader.h" +#include "scheduler.h" + +using namespace volta::agent; +static void on_signal(int) { g_running = 0; } + +void install_signal_handlers() { + struct sigaction sa{}; + sa.sa_handler = on_signal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); +} + +int cli_mode(int argc, const char* argv[]) { + if (argc < 2) return 1; + + MessageQueue msq("/volta_agent_command_queue", MessageQueue::Role::Sender, + {}); + std::string command(argv[1]); + + if (command == "export") { + if (argc < 3) return 1; + std::string subcommand(argv[2]); + + if (subcommand == "start") { + std::string file(""); + if (argc >= 4) { + std::filesystem::path fp(argv[3]); + file = std::filesystem::absolute(fp).string(); + } + std::string message = std::format("dump_start;{}", file); + // std::string message = "dump_start"; + msq.send(message); + std::cout << "Message " << message << " sent" << std::endl; + return 0; + } + + if (subcommand == "stop") { + msq.send("dump_end"); + std::cout << "Message dump_end sent" << std::endl; + return 0; + } + + return 1; + } + + return 1; +} + +int agent_mode() { + install_signal_handlers(); + try { + auto config = config::ConfigLoader::LoadConfig(); + + auto active_collectors = collectors::CollectorRegistry::Instance().Resolve( + config.requestedMetrics); + + std::shared_ptr buffer = + std::make_shared(config); + Scheduler scheduler(config, std::move(active_collectors), buffer); + + std::jthread grpc_thread([&scheduler, &config, &buffer]() { + if (auto channel = client::Client::CreateChannel( + config.server_address + ":" + + std::to_string(config.server_port))) { + client::Client grpc_client(channel, config, buffer); + grpc_client.Connect(); + } else { + std::cerr + << "Failed to create gRPC channel, falling back to local dashboard" + << std::endl; + scheduler.print_dashboard.store(true); + } + }); + + scheduler.Run(); + + } catch (const std::exception& e) { + std::cerr << "CRITICAL ERROR: " << e.what() << std::endl; + return 1; + } + std::cout << "Agent exited successfully" << std::endl; + return 0; +} + +int main(int argc, const char* argv[]) { + std::string program_name(argv[0]); + + if (program_name.ends_with("volta")) + return cli_mode(argc, argv); + else if (program_name.ends_with("voltad")) + return agent_mode(); + return 1; +} diff --git a/sources/agent/src/message_queue.cc b/sources/agent/src/message_queue.cc new file mode 100644 index 0000000..38bbec7 --- /dev/null +++ b/sources/agent/src/message_queue.cc @@ -0,0 +1,130 @@ +#include "message_queue.h" + +#include + +#include +#include +#include +#include + +MessageQueue::MessageQueue(const std::string& name, Role role, + const Options& opts) + : name_(name), role_(role), opts_(opts) { + if (name.empty() || name[0] != '/') + throw std::invalid_argument("Queue name must start with '/'"); + + open_queue(); +} + +MessageQueue::~MessageQueue() { + stop_listening(); + if (fd_ != static_cast(-1)) { + mq_close(fd_); + if (role_ == Role::Receiver) mq_unlink(name_.c_str()); + } +} + +void MessageQueue::open_queue() { + if (role_ == Role::Receiver) mq_unlink(name_.c_str()); + struct mq_attr attr{}; + attr.mq_flags = 0; + attr.mq_maxmsg = opts_.max_messages; + attr.mq_msgsize = opts_.max_msg_size; + attr.mq_curmsgs = 0; + + int flags = (role_ == Role::Receiver) ? (O_RDONLY | O_CREAT | O_NONBLOCK) + : (O_WRONLY | O_NONBLOCK); + + fd_ = ::mq_open(name_.c_str(), flags, opts_.permissions, &attr); + + if (fd_ == static_cast(-1)) { + throw std::runtime_error(std::string("mq_open failed for '") + name_ + + "': " + std::strerror(errno)); + } +} + +void MessageQueue::arm_notify() { + struct sigevent sev{}; + sev.sigev_notify = SIGEV_THREAD; + sev.sigev_notify_function = &MessageQueue::on_notify; + sev.sigev_notify_attributes = nullptr; // default thread attributes + sev.sigev_value.sival_ptr = this; + + if (mq_notify(fd_, &sev) == -1) + throw std::runtime_error(std::string("mq_notify failed: ") + + std::strerror(errno)); +} + +void MessageQueue::drain_and_rearm() { + std::string buf(opts_.max_msg_size, '\0'); + + // Drain every message currently in the queue before re-arming. + // mq_notify only fires on the empty->non-empty transition, so we must + // consume everything now or we will miss messages added while we were + // in this callback. + while (true) { + unsigned int priority = 0; + ssize_t n = ::mq_receive(fd_, buf.data(), buf.size(), &priority); + if (n == -1) { + if (errno == EAGAIN) break; // queue is empty — done + listening_ = false; + return; + } + + if (handler_) + handler_(std::string_view(buf.data(), static_cast(n))); + } + + if (listening_) arm_notify(); +} + +void MessageQueue::on_notify(union sigval sv) { + auto* self = static_cast(sv.sival_ptr); + self->drain_and_rearm(); +} + +bool MessageQueue::send(std::string_view message, unsigned int priority) { + if (role_ != Role::Sender) + throw std::logic_error("send() called on a Receiver queue"); + if (message.size() > static_cast(opts_.max_msg_size)) + throw std::invalid_argument("Message exceeds max_msg_size"); + + return mq_send(fd_, message.data(), message.size(), priority) == 0; +} + +bool MessageQueue::send_nonblocking(std::string_view message, + unsigned int priority) { + if (role_ != Role::Sender) + throw std::logic_error("send_nonblocking() called on a Receiver queue"); + if (message.size() > static_cast(opts_.max_msg_size)) + throw std::invalid_argument("Message exceeds max_msg_size"); + + struct timespec ts = {0, 0}; + return mq_timedsend(fd_, message.data(), message.size(), priority, &ts) == 0; +} + +void MessageQueue::listen(MessageHandler handler) { + if (role_ != Role::Receiver) + throw std::logic_error("listen() called on a Sender queue"); + if (listening_) return; + + handler_ = std::move(handler); + listening_ = true; + arm_notify(); +} + +void MessageQueue::stop_listening() { + if (!listening_) return; + listening_ = false; + // Disarm any pending notification. + mq_notify(fd_, nullptr); + mq_unlink(name_.c_str()); +} + +long MessageQueue::pending_count() const { + struct mq_attr attr{}; + if (mq_getattr(fd_, &attr) == -1) + throw std::runtime_error(std::string("mq_getattr failed: ") + + std::strerror(errno)); + return attr.mq_curmsgs; +} diff --git a/sources/agent/src/message_queue.h b/sources/agent/src/message_queue.h new file mode 100644 index 0000000..155aa84 --- /dev/null +++ b/sources/agent/src/message_queue.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include +#include + +class MessageQueue { + public: + enum class Role { Sender, Receiver }; + + using MessageHandler = std::function; + + struct Options { + long max_messages = 10; + long max_msg_size = 4096; + int permissions = 0660; + }; + + MessageQueue(const std::string& name, Role role, const Options& opts); + ~MessageQueue(); + + MessageQueue(const MessageQueue&) = delete; + MessageQueue& operator=(const MessageQueue&) = delete; + + // Sender API + + // Blocking send. Returns false and sets errno on failure. + bool send(std::string_view message, unsigned int priority = 0); + + // Non-blocking send. Returns false immediately if the queue is full. + bool send_nonblocking(std::string_view message, unsigned int priority = 0); + + // Receiver API + + void listen(MessageHandler handler); + + void stop_listening(); + + bool is_listening() const { return listening_; } + + long pending_count() const; + + const std::string& name() const { return name_; } + + private: + void open_queue(); + void arm_notify(); + void drain_and_rearm(); + static void on_notify(union sigval sv); + + std::string name_; + Role role_; + Options opts_; + mqd_t fd_ = static_cast(-1); + MessageHandler handler_; + bool listening_ = false; +}; diff --git a/sources/agent/src/metric.h b/sources/agent/src/metric.h new file mode 100644 index 0000000..eca82fe --- /dev/null +++ b/sources/agent/src/metric.h @@ -0,0 +1,27 @@ +#ifndef VOLTA_AGENT_SRC_METRIC_H +#define VOLTA_AGENT_SRC_METRIC_H + +#include +#include +#include + +#include "volta.pb.h" + +namespace volta { +namespace agent { + +using DeviceId = std::variant; + +struct Metric { + MetricType type = MetricType::METRIC_TYPE_UNSPECIFIED; + std::optional devId; + double value = 0.0; + int64_t timestamp = 0; + + bool HasDevice() const { return devId.has_value(); } +}; + +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_METRIC_H diff --git a/sources/agent/src/platform/cpu_info.h b/sources/agent/src/platform/cpu_info.h new file mode 100644 index 0000000..a99c04a --- /dev/null +++ b/sources/agent/src/platform/cpu_info.h @@ -0,0 +1,42 @@ +#ifndef VOLTA_AGENT_PLATFORM_CPU_INFO_H_ +#define VOLTA_AGENT_PLATFORM_CPU_INFO_H_ + +#include +#include + +namespace volta { +namespace agent { +namespace platform { + +enum class CpuVendor { UNKNOWN, AMD, INTEL }; + +struct CpuInfo { + CpuVendor vendor = CpuVendor::UNKNOWN; + + std::string model_name; + std::string architecture; + + // Topology information + uint8_t physical_cores = 0; + uint8_t logical_threads = 0; + uint8_t socket_count = 1; + + float base_frequency_mhz = 0.0; + float max_frequency_mhz = 0.0; + + uint64_t l1_cache_size_bytes = 0; + uint64_t l2_cache_size_bytes = 0; + uint64_t l3_cache_size_bytes = 0; + + float power_limit_watts = 0.0; + + bool has_rapl = false; // Support Intel RAPL + bool has_zenpower = false; // Support AMD zenpower/k10temp + bool has_pmu = false; // Access to Performance Monitoring Unit +}; + +} // namespace platform +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_PLATFORM_CPU_INFO_H_ diff --git a/sources/agent/src/platform/gpu_info.h b/sources/agent/src/platform/gpu_info.h new file mode 100644 index 0000000..e1e24c7 --- /dev/null +++ b/sources/agent/src/platform/gpu_info.h @@ -0,0 +1,31 @@ +#ifndef VOLTA_AGENT_PLATFORM_GPU_INFO_H_ +#define VOLTA_AGENT_PLATFORM_GPU_INFO_H_ + +#include +#include + +namespace volta { +namespace agent { +namespace platform { + +enum class GpuVendor { UNKNOWN, NVIDIA, AMD, INTEL }; + +struct GpuInfo { + GpuVendor vendor = GpuVendor::UNKNOWN; + + std::string model_name; + + // Unique identifier (Domain:Bus:Device.Function) to differentiate multiple + // GPUs in one system + std::string pci_address; + std::string driver_version; + + uint64_t vram_total_bytes = 0; + float power_limit_watts = 0.0; +}; + +} // namespace platform +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_PLATFORM_GPU_INFO_H_ diff --git a/source/agent/src/platform/hardware_info.h b/sources/agent/src/platform/hardware_info.h similarity index 59% rename from source/agent/src/platform/hardware_info.h rename to sources/agent/src/platform/hardware_info.h index d15f7e3..944dc4b 100644 --- a/source/agent/src/platform/hardware_info.h +++ b/sources/agent/src/platform/hardware_info.h @@ -1,30 +1,30 @@ #ifndef VOLTA_AGENT_PLATFORM_HARDWARE_INFO_H_ #define VOLTA_AGENT_PLATFORM_HARDWARE_INFO_H_ +#include #include #include -#include -#include "platform/gpu_info.h" #include "platform/cpu_info.h" +#include "platform/gpu_info.h" namespace volta { namespace agent { namespace platform { struct HardwareInfo { - std::string hostname; - std::string os_version; - std::string kernel_version; + std::string hostname; + std::string os_version; + std::string kernel_version; - uint64_t total_ram_bytes = 0; + uint64_t total_ram_bytes = 0; - CpuInfo cpu; - std::vector gpus; + CpuInfo cpu; + std::vector gpus; }; -} // namespace platform -} // namespace agent -} // namespace volta +} // namespace platform +} // namespace agent +} // namespace volta #endif // VOLTA_AGENT_PLATFORM_HARDWARE_INFO_H_ diff --git a/sources/agent/src/platform/nvml_loader.cc b/sources/agent/src/platform/nvml_loader.cc new file mode 100644 index 0000000..498b570 --- /dev/null +++ b/sources/agent/src/platform/nvml_loader.cc @@ -0,0 +1,102 @@ +#include "platform/nvml_loader.h" + +#include +#include + +#include +#include + +namespace volta { +namespace agent { +namespace platform { +namespace { + +NvmlApi g_nvml; +std::string g_load_error; +void* g_nvml_handle = nullptr; + +template +bool ResolveSymbol(void* handle, const char* symbol, T* out) { + dlerror(); + auto fn = reinterpret_cast(dlsym(handle, symbol)); + const char* error = dlerror(); + if (error != nullptr || fn == nullptr) { + g_load_error = fmt::format("dlsym({}): {}", symbol, + error != nullptr ? error : "null symbol"); + return false; + } + *out = fn; + return true; +} + +constexpr std::string_view kLibraryCandidates[] = { + "libnvidia-ml.so.1", + "libnvidia-ml.so", +}; + +bool ResolveAllSymbols(void* handle) { + return ResolveSymbol(handle, "nvmlInit_v2", &g_nvml.Init) && + ResolveSymbol(handle, "nvmlShutdown", &g_nvml.Shutdown) && + ResolveSymbol(handle, "nvmlErrorString", &g_nvml.ErrorString) && + ResolveSymbol(handle, "nvmlDeviceGetCount_v2", + &g_nvml.DeviceGetCount) && + ResolveSymbol(handle, "nvmlDeviceGetHandleByIndex_v2", + &g_nvml.DeviceGetHandleByIndex) && + ResolveSymbol(handle, "nvmlDeviceGetPciInfo_v3", + &g_nvml.DeviceGetPciInfo) && + ResolveSymbol(handle, "nvmlDeviceGetPowerUsage", + &g_nvml.DeviceGetPowerUsage) && + ResolveSymbol(handle, "nvmlDeviceGetTemperature", + &g_nvml.DeviceGetTemperature) && + ResolveSymbol(handle, "nvmlDeviceGetUtilizationRates", + &g_nvml.DeviceGetUtilizationRates) && + ResolveSymbol(handle, "nvmlDeviceGetMemoryInfo", + &g_nvml.DeviceGetMemoryInfo) && + ResolveSymbol(handle, "nvmlDeviceGetName", &g_nvml.DeviceGetName); +} + +} // namespace + +std::string_view NvmlLoadError() { return g_load_error; } + +const NvmlApi* TryLoadNvml() { + if (g_nvml_handle != nullptr) { + return &g_nvml; + } + + void* handle = nullptr; + for (const auto candidate : kLibraryCandidates) { + dlerror(); + handle = dlopen(std::string(candidate).c_str(), RTLD_LAZY | RTLD_LOCAL); + if (handle != nullptr) { + g_nvml.library_path = candidate; + break; + } + if (g_load_error.empty()) { + const char* error = dlerror(); + if (error != nullptr) { + g_load_error = error; + } + } + } + + if (handle == nullptr) { + if (g_load_error.empty()) { + g_load_error = "libnvidia-ml not found in search paths"; + } + return nullptr; + } + + if (!ResolveAllSymbols(handle)) { + dlclose(handle); + return nullptr; + } + + g_nvml_handle = handle; + g_load_error.clear(); + return &g_nvml; +} + +} // namespace platform +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/platform/nvml_loader.h b/sources/agent/src/platform/nvml_loader.h new file mode 100644 index 0000000..98600d9 --- /dev/null +++ b/sources/agent/src/platform/nvml_loader.h @@ -0,0 +1,42 @@ +#ifndef VOLTA_AGENT_SRC_PLATFORM_NVML_LOADER_H_ +#define VOLTA_AGENT_SRC_PLATFORM_NVML_LOADER_H_ + +#ifndef NVML_NO_UNVERSIONED_FUNC_DEFS +#define NVML_NO_UNVERSIONED_FUNC_DEFS +#endif +#include + +#include +#include + +namespace volta { +namespace agent { +namespace platform { + +struct NvmlApi { + std::string library_path; + + nvmlReturn_t (*Init)(void) = nullptr; + nvmlReturn_t (*Shutdown)(void) = nullptr; + const char* (*ErrorString)(nvmlReturn_t) = nullptr; + nvmlReturn_t (*DeviceGetCount)(unsigned int*) = nullptr; + nvmlReturn_t (*DeviceGetHandleByIndex)(unsigned int, nvmlDevice_t*) = nullptr; + nvmlReturn_t (*DeviceGetPciInfo)(nvmlDevice_t, nvmlPciInfo_t*) = nullptr; + nvmlReturn_t (*DeviceGetPowerUsage)(nvmlDevice_t, unsigned int*) = nullptr; + nvmlReturn_t (*DeviceGetTemperature)(nvmlDevice_t, nvmlTemperatureSensors_t, + unsigned int*) = nullptr; + nvmlReturn_t (*DeviceGetUtilizationRates)(nvmlDevice_t, + nvmlUtilization_t*) = nullptr; + nvmlReturn_t (*DeviceGetMemoryInfo)(nvmlDevice_t, nvmlMemory_t*) = nullptr; + nvmlReturn_t (*DeviceGetName)(nvmlDevice_t, char*, unsigned int) = nullptr; +}; + +const NvmlApi* TryLoadNvml(); + +std::string_view NvmlLoadError(); + +} // namespace platform +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_PLATFORM_NVML_LOADER_H_ diff --git a/sources/agent/src/platform/platform_detector.cc b/sources/agent/src/platform/platform_detector.cc new file mode 100644 index 0000000..2de7fbd --- /dev/null +++ b/sources/agent/src/platform/platform_detector.cc @@ -0,0 +1,140 @@ +#include "platform/platform_detector.h" + +#include + +#include +#include +#include +#include + +#ifdef HAVE_NVML +#include +#endif + +namespace volta { +namespace agent { +namespace platform { + +static std::string CpuVendorToString(CpuVendor v) { + switch (v) { + case CpuVendor::INTEL: + return "Intel"; + case CpuVendor::AMD: + return "AMD"; + default: + return "Unknown"; + } +} + +static std::string GpuVendorToString(GpuVendor v) { + switch (v) { + case GpuVendor::NVIDIA: + return "NVIDIA"; + case GpuVendor::AMD: + return "AMD"; + case GpuVendor::INTEL: + return "Intel"; + default: + return "Unknown"; + } +} + +HardwareInfo PlatformDetector::Detect() { + HardwareInfo info; + + char hostname_buffer[256]; + if (gethostname(hostname_buffer, sizeof(hostname_buffer)) == 0) + info.hostname = std::string(hostname_buffer); + else + info.hostname = "unknown-host"; + + info.os_version = DetectOS(); + + std::ifstream cpuinfo("/proc/cpuinfo"); + std::string line; + while (std::getline(cpuinfo, line)) { + if (line.find("vendor_id") != std::string::npos) { + if (line.find("AuthenticAMD") != std::string::npos) + info.cpu.vendor = CpuVendor::AMD; + else if (line.find("GenuineIntel") != std::string::npos) + info.cpu.vendor = CpuVendor::INTEL; + } + if (line.find("model name") != std::string::npos) { + size_t pos = line.find(":"); + if (pos != std::string::npos) { + info.cpu.model_name = line.substr(pos + 2); + } + break; + } + } + +#ifdef HAVE_NVML + if (nvmlInit() == NVML_SUCCESS) { + unsigned int device_count = 0; + nvmlDeviceGetCount(&device_count); + + for (unsigned int i = 0; i < device_count; ++i) { + nvmlDevice_t device; + nvmlDeviceGetHandleByIndex(i, &device); + + char name[64]; + if (nvmlDeviceGetName(device, name, sizeof(name)) == NVML_SUCCESS) { + GpuInfo gpu; + gpu.vendor = GpuVendor::NVIDIA; + gpu.model_name = std::string(name); + info.gpus.push_back(gpu); + } + } + nvmlShutdown(); + } +#endif + + return info; +} + +void PlatformDetector::PrintDetectedInfo(const HardwareInfo& info) { + std::cout << "[" << typeid(*this).name() << "] Hardware Detection Result:\n"; + std::cout << " > Host: " << info.hostname << "\n"; + std::cout << " > OS: " << info.os_version << "\n"; + std::cout << " > CPU: " << CpuVendorToString(info.cpu.vendor) << "\n" + << " | Model: " << info.cpu.model_name << "\n"; + + if (info.gpus.empty()) { + std::cout << " > GPU: None detected (or NVML disabled)\n"; + } else { + for (const auto& gpu : info.gpus) { + std::cout << " > GPU: " << GpuVendorToString(gpu.vendor) << "\n" + << " | Model: " << gpu.model_name << "\n"; + } + } + std::cout << "--------------------------------------------------\n"; + std::cout.flush(); + std::cin.get(); +} + +std::string PlatformDetector::DetectOS() { + std::ifstream file("/etc/os-release"); + + if (!file.is_open()) { + return "Linux (Unknown - Cannot open /etc/os-release)"; + } + + std::string line; + while (std::getline(file, line)) { + if (line.starts_with("PRETTY_NAME=")) { + std::string value = line.substr(12); + + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { + value = value.substr(1, value.size() - 2); + } + + return value; + } + } + + return "Linux (Unknown - Tag not found)"; +} + +} // namespace platform +} // namespace agent +} // namespace volta diff --git a/source/agent/src/platform/platform_detector.h b/sources/agent/src/platform/platform_detector.h similarity index 56% rename from source/agent/src/platform/platform_detector.h rename to sources/agent/src/platform/platform_detector.h index e1af149..88775ce 100644 --- a/source/agent/src/platform/platform_detector.h +++ b/sources/agent/src/platform/platform_detector.h @@ -2,8 +2,6 @@ #define VOLTA_AGENT_PLATFORM_PLATFORM_DETECTOR_H_ #include -#include -#include #include "platform/hardware_info.h" @@ -12,16 +10,16 @@ namespace agent { namespace platform { class PlatformDetector { -public: - HardwareInfo Detect(); - void PrintDetectedInfo(const HardwareInfo& info); + public: + HardwareInfo Detect(); + void PrintDetectedInfo(const HardwareInfo& info); -private: - std::string DetectOS(); + private: + std::string DetectOS(); }; -} // namespace platform -} // namespace agent -} // namespace volta +} // namespace platform +} // namespace agent +} // namespace volta #endif // VOLTA_AGENT_PLATFORM_PLATFORM_DETECTOR_H_ diff --git a/sources/agent/src/scheduler.cc b/sources/agent/src/scheduler.cc new file mode 100644 index 0000000..b7c58b3 --- /dev/null +++ b/sources/agent/src/scheduler.cc @@ -0,0 +1,122 @@ +#include "scheduler.h" + +#include +#include +#include +#include +#include + +volatile sig_atomic_t g_running = 1; + +namespace volta { +namespace agent { + +Scheduler::Scheduler(const config::Config& config, + std::vector&& collectors, + std::shared_ptr buffer) + : collectors_(std::move(collectors)), + config_(config), + buffer_(buffer), + exporter_(config), + ms_queue_("/volta_agent_command_queue", MessageQueue::Role::Receiver, + {}) {} + +void Scheduler::Run() { + for (auto collector : collectors_) { + collector->Init(); + } + + std::cout << "Starting collection loop (Interval: " + << config_.collection_interval.count() << "ms)..." << std::endl; + ms_queue_.listen([this](std::string_view message) { + if (message.starts_with("dump_start")) { + std::optional path = std::nullopt; + if (message.size() > 11) { + int path_start = message.find(";"); + message.remove_prefix(path_start + 1); + path = message; + } + std::cout << "path: " << *path << std::endl; + this->exporter_.StartDump(path); + } + if (message.starts_with("dump_end")) { + this->exporter_.EndDump(); + } + }); + while (g_running) { + for (const auto& collector : collectors_) { + // TODO: make the metrics collect into a preallocated tray + // instead of allocating new memory for each collection + auto metrics = collector->Collect(); + buffer_->AddMetrics(metrics); + exporter_.Dump(metrics); + } + + if (print_dashboard.load()) PrintDashboard(); + + std::this_thread::sleep_for(config_.collection_interval); + } + exporter_.EndDump(); + ms_queue_.stop_listening(); +} + +std::string Scheduler::DescribeKey(const BufferKey& key) { + std::ostringstream out; + if (key.pci_domain || key.pci_bus || key.pci_device || key.pci_function) { + out << "gpu=" << key.pci_domain << ':' << static_cast(key.pci_bus) + << ':' << static_cast(key.pci_device) << '.' + << static_cast(key.pci_function); + } else if (key.socket_index || key.core_index) { + out << "cpu=" << static_cast(key.socket_index) << "/" + << key.core_index; + } else if (key.ifindex) { + out << "net_ifindex=" << key.ifindex; + } else if (key.disk_major || key.disk_minor) { + out << "disk=" << key.disk_major << ':' << key.disk_minor; + } else { + out << "system"; + } + return out.str(); +} + +void Scheduler::PrintDashboard() { + // ansi clean screen, move cursor to top-left + std::cout << "\033[2J\033[1;1H"; + + std::cout << "===============================================\n"; + std::cout << " VOLTA AGENT - ACTIVE MONITOR \n"; + std::cout << "===============================================\n"; + + std::cout << std::left << std::setw(42) << "METRIC NAME" << std::setw(38) + << "DEVICE" + << " " << std::fixed << "VALUE" + << "\n"; + std::cout << "-----------------------------------------------\n"; + + auto latest = buffer_->LatestSamples(); + for (const auto& [key, sample] : latest) { + auto metric_name = + MetricType_Name(static_cast(key.metric_type)); + const std::string prefix = "METRIC_TYPE_"; + if (metric_name.rfind(prefix, 0) == 0) { + metric_name = metric_name.substr(prefix.size()); + } + + std::cout << std::left << std::setw(42) << metric_name << std::setw(38) + << DescribeKey(key) << " " << std::fixed + << std::setprecision(2) << sample.value << " @ " + << sample.timestamp_ns << "\n"; + } + + std::cout << "-----------------------------------------------\n"; + std::cout << "Data points collected: " << latest.size() << "\n"; + if (exporter_.IsActive()) std::cout << "Export in progress... \n"; + std::cout << "# of metrics buffered: " << buffer_->CapacityPerSeries() + << "\n"; + std::cout << "Press Ctrl+C to exit." + << "\n"; + std::cout.flush(); +} + +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/scheduler.h b/sources/agent/src/scheduler.h new file mode 100644 index 0000000..97cf794 --- /dev/null +++ b/sources/agent/src/scheduler.h @@ -0,0 +1,45 @@ +#ifndef VOLTA_AGENT_SRC_SCHEDULER_H_ +#define VOLTA_AGENT_SRC_SCHEDULER_H_ + +#include +#include +#include +#include + +#include "buffer.h" +#include "collectors/collector.h" +#include "config/config.h" +#include "exporter.h" +#include "message_queue.h" +#include "metric.h" + +extern volatile sig_atomic_t g_running; + +namespace volta { +namespace agent { + +class Scheduler { + public: + explicit Scheduler(const config::Config& config, + std::vector&& collectors, + std::shared_ptr buffer); + + void Run(); + + std::atomic print_dashboard = false; + + private: + void PrintDashboard(); + static std::string DescribeKey(const BufferKey& key); + + std::vector collectors_; + const config::Config& config_; + std::shared_ptr buffer_; + MessageQueue ms_queue_; + Exporter exporter_; +}; + +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_SRC_SCHEDULER_H_ diff --git a/sources/agent/src/utils/utils.cc b/sources/agent/src/utils/utils.cc new file mode 100644 index 0000000..153bacf --- /dev/null +++ b/sources/agent/src/utils/utils.cc @@ -0,0 +1,76 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace volta { +namespace agent { +namespace utils { + +void PrintCurrentAffinity() { + cpu_set_t set; + CPU_ZERO(&set); + + if (sched_getaffinity(0, sizeof(set), &set) != 0) { + // TODO: Log + perror("sched_getaffinity"); + return; + } + + long max_cpus = sysconf(_SC_NPROCESSORS_CONF); + std::cout << "Current CPU affinity: "; + + for (int i = 0; i < max_cpus; ++i) { + if (CPU_ISSET(i, &set)) std::cout << i << " "; + } + std::cout << "\n"; +} + +bool IsValidIP(const std::string& ip) { + sockaddr_in sa4{}; + sockaddr_in6 sa6{}; + + return inet_pton(AF_INET, ip.c_str(), &sa4.sin_addr) == 1 || + inet_pton(AF_INET6, ip.c_str(), &sa6.sin6_addr) == 1; +} + +bool IsResolvable(const std::string& host) { + addrinfo hints{}, *res = nullptr; + hints.ai_family = AF_UNSPEC; + + bool ok = getaddrinfo(host.c_str(), nullptr, &hints, &res) == 0; + freeaddrinfo(res); + return ok; +} + +std::string GenerateUUIDv4() { + std::random_device rd; + std::mt19937_64 gen(rd()); + std::uniform_int_distribution dist; + + uint64_t a = dist(gen); + uint64_t b = dist(gen); + + // version 4 + a = (a & 0xffffffffffff0fffULL) | 0x0000000000004000ULL; + // variant 1 (RFC 4122) + b = (b & 0x3fffffffffffffffULL) | 0x8000000000000000ULL; + + std::ostringstream oss; + oss << std::hex << std::setfill('0') << std::setw(8) << (a >> 32) << "-" + << std::setw(4) << ((a >> 16) & 0xffff) << "-" << std::setw(4) + << (a & 0xffff) << "-" << std::setw(4) << (b >> 48) << "-" + << std::setw(12) << (b & 0x0000ffffffffffffULL); + + return oss.str(); +} + +} // namespace utils +} // namespace agent +} // namespace volta diff --git a/sources/agent/src/utils/utils.h b/sources/agent/src/utils/utils.h new file mode 100644 index 0000000..bb18dfa --- /dev/null +++ b/sources/agent/src/utils/utils.h @@ -0,0 +1,19 @@ +#ifndef VOLTA_AGENT_UTILS_UTILS_H_ +#define VOLTA_AGENT_UTILS_UTILS_H_ + +#include + +namespace volta { +namespace agent { +namespace utils { + +void PrintCurrentAffinity(); +bool IsValidIP(const std::string& ip); +bool IsResolvable(const std::string& host); +std::string GenerateUUIDv4(); + +} // namespace utils +} // namespace agent +} // namespace volta + +#endif // VOLTA_AGENT_UTILS_UTILS_H_ diff --git a/sources/agent/vcpkg.json b/sources/agent/vcpkg.json new file mode 100644 index 0000000..f7b4c46 --- /dev/null +++ b/sources/agent/vcpkg.json @@ -0,0 +1,11 @@ +{ + "name": "volta-agent", + "version-string": "0.1.0", + "builtin-baseline": "6d7bf7ef2193e2d1c5798a5ff8811d533104c861", + "dependencies": [ + "fmt", + "grpc", + "protobuf", + "tomlplusplus" + ] +} diff --git a/sources/dashboard/.gitignore b/sources/dashboard/.gitignore new file mode 100644 index 0000000..61cb0c2 --- /dev/null +++ b/sources/dashboard/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/sources/dashboard/README.md b/sources/dashboard/README.md new file mode 100644 index 0000000..c499587 --- /dev/null +++ b/sources/dashboard/README.md @@ -0,0 +1,38 @@ +# Volta Dashboard + +Web UI for visualising collected agent metrics. React 19 SPA that talks to the Volta server over HTTP + WebSocket. + +## Stack + +Vite · React 19 (+ React Compiler) · TypeScript · pnpm · Tailwind v4 · shadcn (on Base UI) · Zustand · uPlot · `@bufbuild/protobuf`. + +## Develop + +```bash +vp install # or: use pnpm directly +vp dev # serves on :5173 +vp build # outputs to dist/ +vp preview # serve the built output for testing +``` + +## Deploy + +The dashboard builds to **standalone static files**. Host the `dist/` output anywhere; the user enters their server URL on the connection screen. + +## How it talks to the server (live-only) + +1. `GET /api/agents` — probe + list connected agents. +2. Open `WS /ws?agent_id={id}`, then `POST /api/agents/{id}/stream` to start streaming. +3. The socket pushes binary protobuf `MetricBatch` frames, decoded with `@bufbuild/protobuf`. + +There is **no history/persistence** yet — the UI shows a rolling live tail only. + +## Layout + +- `src/app/` — entry, provider, router, route pages +- `src/components/` — shared + shadcn `ui/` components, charts, layout +- `src/features/` — domain logic (`metrics/`, `agents/`, `connection/`) +- `src/stores/` — Zustand stores (connection, agent, metrics, ui) +- `src/api/client.ts` — HTTP + WS URL helpers and fetchers +- `src/lib/` — formatting, colors, stats, metric metadata +- `src/proto/gen/` — protobuf codegen (do not edit by hand) diff --git a/sources/dashboard/buf.gen.yaml b/sources/dashboard/buf.gen.yaml new file mode 100644 index 0000000..2a6d35f --- /dev/null +++ b/sources/dashboard/buf.gen.yaml @@ -0,0 +1,16 @@ +# Generates JS + type declarations from the shared proto definitions in ../proto. +# Run via `pnpm run proto:gen`. +# +# Output lives in src/proto/gen and is committed so the build does not depend on +# the buf toolchain. js+dts keeps the generated code (and its non-erasable enums) +# out of our strict type-check pass while still providing full types via .d.ts. +# Re-run after editing ../proto/**. +version: v2 +inputs: + - directory: ../proto +plugins: + - local: protoc-gen-es + out: src/proto/gen + opt: + - target=js+dts + - import_extension=js diff --git a/sources/dashboard/components.json b/sources/dashboard/components.json new file mode 100644 index 0000000..d25c9df --- /dev/null +++ b/sources/dashboard/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-lyra", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "phosphor", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/sources/dashboard/index.html b/sources/dashboard/index.html new file mode 100644 index 0000000..984e31d --- /dev/null +++ b/sources/dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + dashboard + + +
+ + + diff --git a/sources/dashboard/package.json b/sources/dashboard/package.json new file mode 100644 index 0000000..2d37061 --- /dev/null +++ b/sources/dashboard/package.json @@ -0,0 +1,47 @@ +{ + "name": "@volta/dashboard", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vp dev", + "build": "tsc -b && vp build", + "lint": "vp lint .", + "preview": "vp preview", + "proto:gen": "buf generate" + }, + "dependencies": { + "@base-ui/react": "^1.5.0", + "@bufbuild/protobuf": "^2.12.0", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "shadcn": "^4.11.0", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.3.0", + "tw-animate-css": "^1.4.0", + "uplot": "^1.6.32", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@bufbuild/buf": "^1.70.0", + "@bufbuild/protoc-gen-es": "^2.12.0", + "@rolldown/plugin-babel": "^0.2.3", + "@types/babel__core": "^7.20.5", + "@types/node": "^24.13.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "babel-plugin-react-compiler": "^1.0.0", + "globals": "^17.6.0", + "typescript": "~6.0.2", + "vite": "catalog:", + "vite-plus": "catalog:" + }, + "packageManager": "pnpm@11.6.0" +} diff --git a/sources/dashboard/pnpm-lock.yaml b/sources/dashboard/pnpm-lock.yaml new file mode 100644 index 0000000..826cb96 --- /dev/null +++ b/sources/dashboard/pnpm-lock.yaml @@ -0,0 +1,4440 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +catalogs: + default: + vite-plus: + specifier: latest + version: 0.1.24 + +overrides: + vite: npm:@voidzero-dev/vite-plus-core@latest + vitest: npm:@voidzero-dev/vite-plus-test@latest + +importers: + + .: + dependencies: + '@base-ui/react': + specifier: ^1.5.0 + version: 1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@bufbuild/protobuf': + specifier: ^2.12.0 + version: 2.12.0 + '@fontsource-variable/jetbrains-mono': + specifier: ^5.2.8 + version: 5.2.8 + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + react: + specifier: ^19.2.6 + version: 19.2.7 + react-dom: + specifier: ^19.2.6 + version: 19.2.7(react@19.2.7) + shadcn: + specifier: ^4.11.0 + version: 4.11.0(typescript@6.0.3) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + tailwindcss: + specifier: ^4.3.0 + version: 4.3.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + uplot: + specifier: ^1.6.32 + version: 1.6.32 + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) + devDependencies: + '@babel/core': + specifier: ^7.29.0 + version: 7.29.7 + '@bufbuild/buf': + specifier: ^1.70.0 + version: 1.70.0 + '@bufbuild/protoc-gen-es': + specifier: ^2.12.0 + version: 2.12.0(@bufbuild/protobuf@2.12.0) + '@rolldown/plugin-babel': + specifier: ^0.2.3 + version: 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(rolldown@1.0.3) + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/node': + specifier: ^24.13.2 + version: 24.13.2 + '@types/react': + specifier: ^19.2.14 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(rolldown@1.0.3))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(babel-plugin-react-compiler@1.0.0) + babel-plugin-react-compiler: + specifier: ^1.0.0 + version: 1.0.0 + globals: + specifier: ^17.6.0 + version: 17.6.0 + typescript: + specifier: ~6.0.2 + version: 6.0.3 + vite: + specifier: npm:@voidzero-dev/vite-plus-core@latest + version: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)' + vite-plus: + specifier: 'catalog:' + version: 0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.29.7': + resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.5.0': + resolution: {integrity: sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.2.9': + resolution: {integrity: sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@bufbuild/buf-darwin-arm64@1.70.0': + resolution: {integrity: sha512-c7owUswBbMmwfHPH9JRBEJu09mrXYGC33V2JQCgraWCBm74Z95AOkhDua50qiBrQnysvJkJ0p/z4MWxJqcpnIA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@bufbuild/buf-darwin-x64@1.70.0': + resolution: {integrity: sha512-sucV3lQXVuOqYs3+ToulkUh2tZuMnl286DKb44imp3PnexVhAVOP7d3ybYe98HNGwysEdjNP2WIOGb0uKuRCIQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@bufbuild/buf-linux-aarch64@1.70.0': + resolution: {integrity: sha512-4viSYqbhIusd6LR+JayDex8S1rLUL+hTUMYUgSPl75EC93FpJM4vkk2RhoAhyjQqWF/JQLcyWV8kjRRiIwygdg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@bufbuild/buf-linux-armv7@1.70.0': + resolution: {integrity: sha512-GqujpTX4MXtYiUkxd6oI1g0JaCX3L6koT16Gl0D0HIQ/V2mptH7x4UW8nK3tAURMjrHsEEhcSJRtmfINTTKnsg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@bufbuild/buf-linux-x64@1.70.0': + resolution: {integrity: sha512-5WHGUIb5iLFXcnqV33TDejqaPgx0CWFaYW7b4wh12wT0w3DR+ghFq6S6RmYyZLbTuhS4ZFsf+xyk5m+HViKxrA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@bufbuild/buf-win32-arm64@1.70.0': + resolution: {integrity: sha512-dU1qh7iD08/1avCHwIOoGsatQctE6uGwgOue9GOaThi8/Rdy1x9CC/eFdyFSeCMwbUg9ABQvMGUocylfUe6xDw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@bufbuild/buf-win32-x64@1.70.0': + resolution: {integrity: sha512-iKYbjTbEk0ppkv2SrsPFhYus93kj/aMN8aRsrpuo91ZVqXg8JcH4XXbgFpwiFAsiABjqKICFfnDomrFvv49UOA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@bufbuild/buf@1.70.0': + resolution: {integrity: sha512-oJWGqltlu8F7VVNHLoJ3pFXhjfiGpbh7+/mXW0y+VMPWFGxc9YDv4de1UcX7zhhjV6MbE4SiEGo5Gs5jhpVg5A==} + engines: {node: '>=12'} + hasBin: true + + '@bufbuild/protobuf@2.12.0': + resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==} + + '@bufbuild/protoc-gen-es@2.12.0': + resolution: {integrity: sha512-d9htF6jEkSwPbp9d/vSmZOBF7eeG18AvTMKmVg4I23afnrQOxL2w3WOXa9TaufMCyu24QakEUb4vux8apI5e7A==} + engines: {node: '>=20'} + hasBin: true + peerDependencies: + '@bufbuild/protobuf': 2.12.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + + '@bufbuild/protoplugin@2.12.0': + resolution: {integrity: sha512-ORlDITp8AFUXzIhLRoMCG+ud+D3MPKWb5HQXBoskMMnjeyEjE1H1qLonVNPyOr8lkx3xSfYUo8a0dvOZJVAzow==} + + '@dotenvx/dotenvx@1.71.2': + resolution: {integrity: sha512-Xj9T3Wr+Bo4ILKf9PZJBYJ4SJiZGC/pqIdzOMbX9jgAFb0oGuKkusLleYHN/N6zanZixNvmuMVWYR1T3YJuVTA==} + hasBin: true + + '@ecies/ciphers@0.2.6': + resolution: {integrity: sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==} + engines: {bun: '>=1', deno: '>=2.7.10', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fontsource-variable/jetbrains-mono@5.2.8': + resolution: {integrity: sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==} + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/runtime@0.133.0': + resolution: {integrity: sha512-PkvjA1Lq5++V5S1E6Patr92ZVcieE6EalDr1VJTqv4BnjZdOUC4W3p8k1wMXSd5/2aFP4b/A6N5sg2Bkzcr9vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@oxfmt/binding-android-arm-eabi@0.52.0': + resolution: {integrity: sha512-17EMSJnQ9g+upVHrAUYDMfH5lvRKQ9Nvg8WtEoH72oDr1VpWz+7/o3tD97U1EToen2YAQ/68JmtDYkQUi20dfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.52.0': + resolution: {integrity: sha512-A2G1IdwGEW2lLJkIxcvuirRH1CzSl/e0NX11zTlW1gvxJThfwbI/BEoaKrTNpm7M2FchvIf6guvIQU7d5iz+OQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.52.0': + resolution: {integrity: sha512-f9+bLvOYxy7NttCLFTvQ7afmqDOWY4wIP9xdvfj5trQ1qj6f2UFAGwZESlfsMjvJNTyRpXfIlOanCI9FOvoeQA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.52.0': + resolution: {integrity: sha512-YSTB9sJ5nnQd/Q0ddHkgof0ZCHPAnWZT1IW2SJ8omz7CP7KluJhO1fNHrpqdxCtpztJwSs4hY1uAee35wKxxaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.52.0': + resolution: {integrity: sha512-NIrRNTTPCs4UbmVs0bxLSCDlLCtIRMJIXklNKaXa5Oj2/K1UIMBvgE8+uPVo01Io3N9HF0+GAX+aAHjUgZS7vA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': + resolution: {integrity: sha512-JXUCde8mn3GpgQouz2PXUokgy/uT1QrRJBL2s983VWcSQp62wTFYiNXgTKdeo1Jgbr0IgUnKKvzIk/YBlj/nVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.52.0': + resolution: {integrity: sha512-psbUXaRZ+V8DaXz10Qf7LSHtdtdKAmC8fxXgeU608jjzrmWK4quamZMOpl6sf+dikoFHA85uE93Q0BqxrCdQrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.52.0': + resolution: {integrity: sha512-Jw7MgWUU9lcLCcy82updISP3EthTlfvAwR6gWNxPzqly7+fLvOi2gHQE9xXQjpqaVLm/8P+gOzlv9ODuoVlaaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.52.0': + resolution: {integrity: sha512-wZg6bLjDvh2KibyI3QFUYo8GTXneIFsd0JvehtvJiUmQ8WRPERgxd/VM4ctWb86U5FT1FkqgS8/wZKVB+AZScg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.52.0': + resolution: {integrity: sha512-IngE8uxhNvxcMrLjZNDo9xNLY7rEK33AKnaMd2B46he1e/mz2CfcW6If/U1wUjdRZddm1QzQaciqZkuMkdh1FA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.52.0': + resolution: {integrity: sha512-H3+DdFMv/efN3Efmhsv18jDrpiWWqKG7wsfAlQBqAt6z/E2Bx+TwEj2Nowe51CPOWB8/mFBC2dAMSgVFLvvowA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.52.0': + resolution: {integrity: sha512-zji+1kb7lJKohSDjzC1IsS+K/cKRs1hdVf0ZH0VbdbiakmtLvN9twBoXo/k8VdjFax7kfo+DyPxS7vv52br1aw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.52.0': + resolution: {integrity: sha512-hcLBYedpCy7ToUvvBidWk7+11Yhg1oAZ4+6hKPic/mQI6NaqXJSXMps5nFlwUuX2ewhtLZZDPg63TI042qGKBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.52.0': + resolution: {integrity: sha512-IDO2loXK2OtTOhSPchU9MW25mWL2QCDGdJbjN8MXKZVS80qXe5gMTwQWu/gMJ3juoBHbkuUZNB2N1LHzNT7DoA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.52.0': + resolution: {integrity: sha512-mAV2Hjn0SatJ+KoAzKUC3eJhdJ8wv+3m1KyuS0dTsbF0c5weq+QrCt/DRZZM+uj/XiKzCDEUKYsBF30e2qkcyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.52.0': + resolution: {integrity: sha512-vd4npaUIwChxp7XzkqmepBWTT9YMcSe/NBApVGPC30/lLyOVaV3dvma1SKo03t8O73BPRAG7EyJzGlN5cJM5hQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.52.0': + resolution: {integrity: sha512-k2sz6gWQdMfh5HPpIS+Bw/0UEV/kaK2xuqJRrWL233sEHx9WLlsmvlPFM4HUNThkYbSN0U0vPW7LVKZWDS8hPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.52.0': + resolution: {integrity: sha512-rhke69GTcArodLHpjMTfNnvjTEBryDeZcUCKK/VjXDMtfTULl6QRh0ymX5/hbCUv2WjYm9h/QbW++q2vE15gWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.52.0': + resolution: {integrity: sha512-q5xL7oeXkZdEtNZWBdvehJcmt+GRu9l2bK40yJs1jJXlqq+r0Hygb1rTjq+FM2o/2xyt4cufH6KRplHp3Jjsvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.23.0': + resolution: {integrity: sha512-gOs9PVr2wEg4ox9z0aJo+RKhhImW86YL5N6yav8BK/rgPsIrwN/igSZ+pbRr723NFvUNKde9fgMhRA6JrXAOZw==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.23.0': + resolution: {integrity: sha512-kjJ8B+7n4tB9VJdxS5A9GdJt6/bYpzbu4lXp2uO1S3sRmCB5gDEABlGoiePNApRWaW+xqL4b4xgiE727jSLhuA==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.23.0': + resolution: {integrity: sha512-6dCZuKNu135seMXilkRk9SpCx6i1XgmiipYGalLij5WVRX6ZYS8c4xI7preN/zv9fCXhsQclTIMDu2Y/cytTjw==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.23.0': + resolution: {integrity: sha512-3bdilnyA7kmSTjK27rvjIjSxL5SIg3wt7vwNiRkouWB83ytssyKnuGvxSYJxgMEmFpSutzaBzcCUM2jDtPGcgA==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.23.0': + resolution: {integrity: sha512-j+OEp44SVYiQ+ZD+uttsX7u6L9SvmbbQ77SO1pSFCcJlsVMeCk8qZsjhKfGKuT/jIA+ipOJMVs/+pqUfObBWNw==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.23.0': + resolution: {integrity: sha512-5MyjFuqf+g8OUPJBSGWHJtmoWnzFJYyOg4To9WMQshZYEWig/vtu7JtJ03VWnzHv9LJkAUeApY0gVCOywFR/iQ==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.67.0': + resolution: {integrity: sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.67.0': + resolution: {integrity: sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.67.0': + resolution: {integrity: sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.67.0': + resolution: {integrity: sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.67.0': + resolution: {integrity: sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.67.0': + resolution: {integrity: sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.67.0': + resolution: {integrity: sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.67.0': + resolution: {integrity: sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.67.0': + resolution: {integrity: sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.67.0': + resolution: {integrity: sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.67.0': + resolution: {integrity: sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.67.0': + resolution: {integrity: sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.67.0': + resolution: {integrity: sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.67.0': + resolution: {integrity: sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.67.0': + resolution: {integrity: sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.67.0': + resolution: {integrity: sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.67.0': + resolution: {integrity: sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.67.0': + resolution: {integrity: sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.67.0': + resolution: {integrity: sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/plugins@1.61.0': + resolution: {integrity: sha512-nkOyZEF1vH527CkdQtOp1HMrVFEM4ResURvI2JFeGoup+h+43J/k/FgdOR9b9Isxg+Yae7qVDa7y3nssE8b3TQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/plugin-babel@0.2.3': + resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==} + engines: {node: '>=22.12.0 || ^24.0.0'} + peerDependencies: + '@babel/core': ^7.29.0 || ^8.0.0-rc.1 + '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1 + '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1 + rolldown: ^1.0.0-rc.5 + vite: ^8.0.0 + peerDependenciesMeta: + '@babel/plugin-transform-runtime': + optional: true + '@babel/runtime': + optional: true + vite: + optional: true + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + + '@typescript/vfs@1.6.4': + resolution: {integrity: sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==} + peerDependencies: + typescript: '*' + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@voidzero-dev/vite-plus-core@0.1.24': + resolution: {integrity: sha512-iXPGBABnQnrDMx89H6MOCGcTZp+QW+3rY4YMVKdE6ydchSvPk2O3MI2vgaRVfOtWJ2IjnxSnf1n2yjP67ZBRFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.1 + '@tsdown/exe': 0.22.1 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.8 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + yaml: + optional: true + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.24': + resolution: {integrity: sha512-Hpo9W9piSFlEsJzGkwzfDXhJGrnYByxHXF7NVQZ7g+SLOprddtlfTeM8t+gq9dxcuq0RzM8ddMAhDQP/K3fZQA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@voidzero-dev/vite-plus-darwin-x64@0.1.24': + resolution: {integrity: sha512-SwnnnZrEFBiU5iKlh/CZAVwn0RFt/Udrvt3kFLtdRxMtN5bKaqTFVA2H8Y/FPCWp1QX9bs4V9ZIAeXAk06zLkw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.24': + resolution: {integrity: sha512-ImM3eqDki4DpRuHjW6dEh4St8zvbcfOMR7KQZJX42ArriCLQ/QdaYhDRRbcDi27XsOBqRxm2eqUUEymPrYIHpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.24': + resolution: {integrity: sha512-gj4mzbob/ls8Zs7iTuF9Gr0EFFF7tdpDiPxDPBkH8tJP5OkHABlzWUwJhU+9xxcUbTaXqpHDw68Mil7jm5dpMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.24': + resolution: {integrity: sha512-x7IYK7lI+WuF1n3jSzEYU6FgJxPX/R0rDmTTsOutooGGCU7uShZvfZqIoiTXK0eFnJU5ij5BfBgenenUfsaT/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.24': + resolution: {integrity: sha512-JCy2w0eSVUlWQlggK5T47MnL+j0o4EY7hLskINVI8gi+aixQF4xnYBDobz0lbxkqz3/IfiLyXUx6TcU3thcsGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@voidzero-dev/vite-plus-test@0.1.24': + resolution: {integrity: sha512-9NiG6UadG0iOaPL1AMsO5sDKkx6MADHw4/mMOmHWZUhhUwqzfVtnnptMK37vD71e6KyR7yAscx19FrtOWWtjvA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.24': + resolution: {integrity: sha512-G+/lhLKVjyn3FmgXX8jeWgq7RcE5O1kdR7QyFayQOdlMX/ZRkvUwQD7bFaqhKzgJM6Oj3a1FH3HQPYk5QOYuCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.24': + resolution: {integrity: sha512-b0e5XohEV1w/RdzAtv8/Hm6tvHPXouPtBNsljjW/lDJZq3NCLND5s6lqe8H4IenrgmKSoqakHWtlqJqM36cFbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.36: + resolution: {integrity: sha512-lVq/Df7LXlO79MVaaUHztSwWiG9oXoWHlgvNS51v8Dpd4+G4/VIy6qYePTw31nAVls33nUtnfezYeLkYAak9dg==} + engines: {node: '>=6.0.0'} + hasBin: true + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eciesjs@0.4.18: + resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.371: + resolution: {integrity: sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.24.0: + resolution: {integrity: sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==} + engines: {node: '>=10.13.0'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.5: + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.12.25: + resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + obug@2.1.2: + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + oxfmt@0.52.0: + resolution: {integrity: sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@0.23.0: + resolution: {integrity: sha512-3mBv3CoPbh8dFbzfDGIWa2ytZjn2v+3EX4aKRXjIhsoGFzG8GCjfRirz3rwZf1wYbZzsNLTSgpw8VjQuWdp/jA==} + hasBin: true + + oxlint@1.67.0: + resolution: {integrity: sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pixelmatch@7.2.0: + resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==} + hasBin: true + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pngjs@7.0.0: + resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} + engines: {node: '>=14.19.0'} + + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn@4.11.0: + resolution: {integrity: sha512-UV0cchFea9hO7poV1CuEP0wvmYjpAqcxCKdy23bndl2Du2ARtDs8A4xdzfhUjDBeOW1nNpJ6lXmsEpsply2SfQ==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-object@5.0.0: + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uplot@1.6.32: + resolution: {integrity: sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@7.0.2: + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite-plus@0.1.24: + resolution: {integrity: sha512-b3fr6WtCiEhetjuzW/4KcEMOAMuZxoxZATWaXKmPzOLf1upG+pzKJOFZTb94D6wiPBlwcjxoaUtF7C3uAN+VjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + 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 + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-spinner@1.2.0: + resolution: {integrity: sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@base-ui/react@1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.2.9(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@base-ui/utils@0.2.9(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + reselect: 5.2.0 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@bufbuild/buf-darwin-arm64@1.70.0': + optional: true + + '@bufbuild/buf-darwin-x64@1.70.0': + optional: true + + '@bufbuild/buf-linux-aarch64@1.70.0': + optional: true + + '@bufbuild/buf-linux-armv7@1.70.0': + optional: true + + '@bufbuild/buf-linux-x64@1.70.0': + optional: true + + '@bufbuild/buf-win32-arm64@1.70.0': + optional: true + + '@bufbuild/buf-win32-x64@1.70.0': + optional: true + + '@bufbuild/buf@1.70.0': + optionalDependencies: + '@bufbuild/buf-darwin-arm64': 1.70.0 + '@bufbuild/buf-darwin-x64': 1.70.0 + '@bufbuild/buf-linux-aarch64': 1.70.0 + '@bufbuild/buf-linux-armv7': 1.70.0 + '@bufbuild/buf-linux-x64': 1.70.0 + '@bufbuild/buf-win32-arm64': 1.70.0 + '@bufbuild/buf-win32-x64': 1.70.0 + + '@bufbuild/protobuf@2.12.0': {} + + '@bufbuild/protoc-gen-es@2.12.0(@bufbuild/protobuf@2.12.0)': + dependencies: + '@bufbuild/protoplugin': 2.12.0 + optionalDependencies: + '@bufbuild/protobuf': 2.12.0 + transitivePeerDependencies: + - supports-color + + '@bufbuild/protoplugin@2.12.0': + dependencies: + '@bufbuild/protobuf': 2.12.0 + '@typescript/vfs': 1.6.4(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@dotenvx/dotenvx@1.71.2': + dependencies: + commander: 11.1.0 + dotenv: 17.4.2 + eciesjs: 0.4.18 + enquirer: 2.4.1 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.4) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.4 + which: 4.0.0 + yocto-spinner: 1.2.0 + + '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.11': {} + + '@fontsource-variable/jetbrains-mono@5.2.8': {} + + '@hono/node-server@1.19.14(hono@4.12.25)': + dependencies: + hono: 4.12.25 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.25) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.25 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/runtime@0.133.0': {} + + '@oxc-project/types@0.133.0': {} + + '@oxfmt/binding-android-arm-eabi@0.52.0': + optional: true + + '@oxfmt/binding-android-arm64@0.52.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.52.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.52.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.52.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.52.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.52.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.52.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.52.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.52.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.52.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.52.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.52.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.52.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.52.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.52.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.52.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.52.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.52.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.23.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.23.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.23.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.23.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.23.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.23.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.67.0': + optional: true + + '@oxlint/binding-android-arm64@1.67.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.67.0': + optional: true + + '@oxlint/binding-darwin-x64@1.67.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.67.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.67.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.67.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.67.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.67.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.67.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.67.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.67.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.67.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.67.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.67.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.67.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.67.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.67.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.67.0': + optional: true + + '@oxlint/plugins@1.61.0': {} + + '@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(rolldown@1.0.3)': + dependencies: + '@babel/core': 7.29.7 + picomatch: 4.0.4 + rolldown: 1.0.3 + optionalDependencies: + '@babel/runtime': 7.29.7 + vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)' + + '@rolldown/pluginutils@1.0.1': {} + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)' + + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.2.5 + path-browserify: 1.0.1 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/node@24.13.2': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/validate-npm-package-name@4.0.2': {} + + '@typescript/vfs@1.6.4(typescript@5.4.5)': + dependencies: + debug: 4.4.3 + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@6.0.2(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(rolldown@1.0.3))(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(babel-plugin-react-compiler@1.0.0)': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)' + optionalDependencies: + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(rolldown@1.0.3) + babel-plugin-react-compiler: 1.0.0 + + '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)': + dependencies: + '@oxc-project/runtime': 0.133.0 + '@oxc-project/types': 0.133.0 + lightningcss: 1.32.0 + postcss: 8.5.15 + optionalDependencies: + '@types/node': 24.13.2 + fsevents: 2.3.3 + jiti: 2.7.0 + typescript: 6.0.3 + + '@voidzero-dev/vite-plus-darwin-arm64@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-darwin-x64@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-test@0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@voidzero-dev/vite-plus-core': 0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3) + es-module-lexer: 1.7.0 + obug: 2.1.2 + pixelmatch: 7.2.0 + pngjs: 7.0.0 + sirv: 3.0.2 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + vite: '@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3)' + ws: 8.21.0 + optionalDependencies: + '@types/node': 24.13.2 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@tsdown/css' + - '@tsdown/exe' + - '@vitejs/devtools' + - bufferutil + - esbuild + - jiti + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - yaml + + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.24': + optional: true + + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.24': + optional: true + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + babel-plugin-react-compiler@1.0.0: + dependencies: + '@babel/types': 7.29.7 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.36: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.36 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.371 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001799: {} + + chalk@5.6.2: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + commander@11.1.0: {} + + commander@14.0.3: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@9.0.2(typescript@6.0.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 6.0.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + data-uri-to-buffer@4.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.2: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + diff@8.0.4: {} + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eciesjs@0.4.18: + dependencies: + '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.371: {} + + emoji-regex@10.6.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.24.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + esprima@4.0.1: {} + + etag@1.8.1: {} + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.2: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@11.3.5: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuzzysort@3.1.0: {} + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-own-enumerable-keys@1.0.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@6.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globals@17.6.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.12.25: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + human-signals@8.0.1: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regexp@3.1.0: {} + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + jiti@2.7.0: {} + + jose@6.2.3: {} + + js-tokens@4.0.0: {} + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json5@2.2.3: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lines-and-columns@1.2.4: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimist@1.2.8: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + negotiator@1.0.0: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.47: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-treeify@1.1.33: {} + + obug@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + oxfmt@0.52.0(vite-plus@0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3)): + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.52.0 + '@oxfmt/binding-android-arm64': 0.52.0 + '@oxfmt/binding-darwin-arm64': 0.52.0 + '@oxfmt/binding-darwin-x64': 0.52.0 + '@oxfmt/binding-freebsd-x64': 0.52.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.52.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.52.0 + '@oxfmt/binding-linux-arm64-gnu': 0.52.0 + '@oxfmt/binding-linux-arm64-musl': 0.52.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.52.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.52.0 + '@oxfmt/binding-linux-riscv64-musl': 0.52.0 + '@oxfmt/binding-linux-s390x-gnu': 0.52.0 + '@oxfmt/binding-linux-x64-gnu': 0.52.0 + '@oxfmt/binding-linux-x64-musl': 0.52.0 + '@oxfmt/binding-openharmony-arm64': 0.52.0 + '@oxfmt/binding-win32-arm64-msvc': 0.52.0 + '@oxfmt/binding-win32-ia32-msvc': 0.52.0 + '@oxfmt/binding-win32-x64-msvc': 0.52.0 + vite-plus: 0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3) + + oxlint-tsgolint@0.23.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.23.0 + '@oxlint-tsgolint/darwin-x64': 0.23.0 + '@oxlint-tsgolint/linux-arm64': 0.23.0 + '@oxlint-tsgolint/linux-x64': 0.23.0 + '@oxlint-tsgolint/win32-arm64': 0.23.0 + '@oxlint-tsgolint/win32-x64': 0.23.0 + + oxlint@1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.67.0 + '@oxlint/binding-android-arm64': 1.67.0 + '@oxlint/binding-darwin-arm64': 1.67.0 + '@oxlint/binding-darwin-x64': 1.67.0 + '@oxlint/binding-freebsd-x64': 1.67.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.67.0 + '@oxlint/binding-linux-arm-musleabihf': 1.67.0 + '@oxlint/binding-linux-arm64-gnu': 1.67.0 + '@oxlint/binding-linux-arm64-musl': 1.67.0 + '@oxlint/binding-linux-ppc64-gnu': 1.67.0 + '@oxlint/binding-linux-riscv64-gnu': 1.67.0 + '@oxlint/binding-linux-riscv64-musl': 1.67.0 + '@oxlint/binding-linux-s390x-gnu': 1.67.0 + '@oxlint/binding-linux-x64-gnu': 1.67.0 + '@oxlint/binding-linux-x64-musl': 1.67.0 + '@oxlint/binding-openharmony-arm64': 1.67.0 + '@oxlint/binding-win32-arm64-msvc': 1.67.0 + '@oxlint/binding-win32-ia32-msvc': 1.67.0 + '@oxlint/binding-win32-x64-msvc': 1.67.0 + oxlint-tsgolint: 0.23.0 + vite-plus: 0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3) + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-to-regexp@8.4.2: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pixelmatch@7.2.0: + dependencies: + pngjs: 7.0.0 + + pkce-challenge@5.0.1: {} + + pngjs@7.0.0: {} + + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react@19.2.7: {} + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + require-from-string@2.0.2: {} + + reselect@5.2.0: {} + + resolve-from@4.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shadcn@4.11.0(typescript@6.0.3): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@dotenvx/dotenvx': 1.71.2 + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@types/validate-npm-package-name': 4.0.2 + browserslist: 4.28.2 + commander: 14.0.3 + cosmiconfig: 9.0.2(typescript@6.0.3) + dedent: 1.7.2 + deepmerge: 4.3.1 + diff: 8.0.4 + execa: 9.6.1 + fast-glob: 3.3.3 + fs-extra: 11.3.5 + fuzzysort: 3.1.0 + https-proxy-agent: 7.0.6 + kleur: 4.1.5 + node-fetch: 3.3.2 + open: 11.0.0 + ora: 8.2.0 + postcss: 8.5.15 + postcss-selector-parser: 7.1.4 + prompts: 2.4.2 + recast: 0.23.11 + stringify-object: 5.0.0 + tailwind-merge: 3.6.0 + ts-morph: 26.0.0 + tsconfig-paths: 4.2.0 + validate-npm-package-name: 7.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - babel-plugin-macros + - supports-color + - typescript + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + stdin-discarder@0.2.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + stringify-object@5.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-obj: 3.0.0 + is-regexp: 3.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@4.0.0: {} + + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@2.1.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.4.5: {} + + typescript@6.0.3: {} + + undici-types@7.18.2: {} + + unicorn-magic@0.3.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uplot@1.6.32: {} + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + + util-deprecate@1.0.2: {} + + validate-npm-package-name@7.0.2: {} + + vary@1.1.2: {} + + vite-plus@0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3): + dependencies: + '@oxc-project/types': 0.133.0 + '@oxlint/plugins': 1.61.0 + '@voidzero-dev/vite-plus-core': 0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3) + '@voidzero-dev/vite-plus-test': 0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3) + oxfmt: 0.52.0(vite-plus@0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3)) + oxlint: 1.67.0(oxlint-tsgolint@0.23.0)(vite-plus@0.1.24(@types/node@24.13.2)(@voidzero-dev/vite-plus-core@0.1.24(@types/node@24.13.2)(jiti@2.7.0)(typescript@6.0.3))(jiti@2.7.0)(typescript@6.0.3)) + oxlint-tsgolint: 0.23.0 + optionalDependencies: + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.24 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.24 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.24 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.24 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.24 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.24 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.24 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.24 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@tsdown/css' + - '@tsdown/exe' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vite + - yaml + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + yallist@3.1.1: {} + + yocto-spinner@1.2.0: + dependencies: + yoctocolors: 2.1.2 + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): + optionalDependencies: + '@types/react': 19.2.17 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) diff --git a/sources/dashboard/pnpm-workspace.yaml b/sources/dashboard/pnpm-workspace.yaml new file mode 100644 index 0000000..f293410 --- /dev/null +++ b/sources/dashboard/pnpm-workspace.yaml @@ -0,0 +1,16 @@ +allowBuilds: + "@bufbuild/buf": true +catalog: + vite: npm:@voidzero-dev/vite-plus-core@latest + vitest: npm:@voidzero-dev/vite-plus-test@latest + vite-plus: latest +overrides: + vite: "catalog:" + vitest: "catalog:" +peerDependencyRules: + allowAny: + - vite + - vitest + allowedVersions: + vite: "*" + vitest: "*" diff --git a/sources/dashboard/public/favicon.svg b/sources/dashboard/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/sources/dashboard/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sources/dashboard/public/icons.svg b/sources/dashboard/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/sources/dashboard/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/dashboard/src/api/client.ts b/sources/dashboard/src/api/client.ts new file mode 100644 index 0000000..04aa7ee --- /dev/null +++ b/sources/dashboard/src/api/client.ts @@ -0,0 +1,55 @@ +import { fromJson } from "@bufbuild/protobuf"; +import { ListAgentsResponseSchema, type Agent } from "@/proto/gen/types/agent_pb"; + +export type { Agent }; + +const trimTrailingSlash = (url: string) => url.replace(/\/+$/, ""); + +export function apiUrl(baseUrl: string, path: string) { + return `${trimTrailingSlash(baseUrl)}${path}`; +} + +export function wsUrl(baseUrl: string, agentId: string) { + const origin = trimTrailingSlash(baseUrl) || window.location.origin; + + const url = new URL(origin); + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; + url.pathname = "/ws"; + url.search = `agent_id=${encodeURIComponent(agentId)}`; + + return url.toString(); +} + +/** List connected agents. */ +export async function fetchAgents(baseUrl: string, signal?: AbortSignal) { + const url = apiUrl(baseUrl, "/api/agents"); + const res = await fetch(url, { + headers: { Accept: "application/json" }, + signal, + }); + + if (!res.ok) { + throw new Error(`Server returned ${res.status} ${res.statusText}`); + } + + const json = await res.json(); + const listAgentsResponse = fromJson(ListAgentsResponseSchema, json, { + ignoreUnknownFields: true, + }); + return listAgentsResponse.agents; +} + +/** Ask an agent to begin streaming. + * Subscribe to /ws first so no opening batches are missed. + **/ +export async function startStream(baseUrl: string, agentId: string) { + const url = apiUrl(baseUrl, `/api/agents/${encodeURIComponent(agentId)}/stream`); + + const res = await fetch(url, { + method: "POST", + }); + + if (!res.ok) { + throw new Error(`Failed to start stream (${res.status})`); + } +} diff --git a/sources/dashboard/src/app/index.tsx b/sources/dashboard/src/app/index.tsx new file mode 100644 index 0000000..1cf3ce0 --- /dev/null +++ b/sources/dashboard/src/app/index.tsx @@ -0,0 +1,12 @@ +import { AppProvider } from "@/app/provider"; +import { AppRouter } from "@/app/router"; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/sources/dashboard/src/app/pages/connection-page.tsx b/sources/dashboard/src/app/pages/connection-page.tsx new file mode 100644 index 0000000..6396035 --- /dev/null +++ b/sources/dashboard/src/app/pages/connection-page.tsx @@ -0,0 +1,77 @@ +import { useState, type FormEvent } from "react"; +import { ChartLineIcon, SpinnerIcon, WarningCircleIcon } from "@phosphor-icons/react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RecentUrls } from "@/features/connection/recent-urls"; +import { + DEFAULT_SERVER_URL, + useBaseUrl, + useConnectionActions, + useConnectionError, + useConnectionStatus, +} from "@/stores/connection-store"; + +export function ConnectionPage() { + const status = useConnectionStatus(); + const lastError = useConnectionError(); + const baseUrl = useBaseUrl(); + const { connect } = useConnectionActions(); + + const [url, setUrl] = useState(baseUrl || DEFAULT_SERVER_URL); + const connecting = status === "connecting"; + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + void connect(url.trim()); + }; + + return ( +
+
+
+
+ + VOLTA +
+ +
+

Connect to a server

+

+ Enter the Volta server URL, or leave blank to use the same origin. +

+
+ +
+ + setUrl(e.target.value)} + placeholder="http://localhost:8080" + autoFocus + spellCheck={false} + autoComplete="off" + /> +
+ + {status === "error" && lastError && ( +
+ + {lastError} +
+ )} + + +
+ + +
+
+ ); +} diff --git a/sources/dashboard/src/app/pages/dashboard-page.tsx b/sources/dashboard/src/app/pages/dashboard-page.tsx new file mode 100644 index 0000000..f76c52c --- /dev/null +++ b/sources/dashboard/src/app/pages/dashboard-page.tsx @@ -0,0 +1,94 @@ +import { useMemo } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { MetricCard } from "@/features/metrics/metric-card"; +import { CATEGORIES } from "@/lib/metrics"; +import { useSeries, useStreamStatus, type LiveSeries } from "@/stores/metrics-store"; +import { useSelectedAgentId } from "@/stores/agent-store"; +import { useUiActions } from "@/stores/ui-store"; + +export function DashboardPage() { + const series = useSeries(); + const streamStatus = useStreamStatus(); + const { openDetail } = useUiActions(); + const selectedAgentId = useSelectedAgentId(); + + const grouped = useMemo(() => { + const byCategory = new Map(); + for (const s of Object.values(series)) { + const list = byCategory.get(s.category) ?? []; + list.push(s); + byCategory.set(s.category, list); + } + for (const list of byCategory.values()) { + list.sort( + (a, b) => + a.label.localeCompare(b.label) || + (a.deviceLabel ?? "").localeCompare(b.deviceLabel ?? ""), + ); + } + return byCategory; + }, [series]); + + if (!selectedAgentId) { + return ( + + ); + } + + if (grouped.size === 0) { + return streamStatus === "open" ? ( + + ) : ( + + ); + } + + return ( +
+ {CATEGORIES.map((category) => { + const list = grouped.get(category.id); + if (!list?.length) return null; + const Icon = category.icon; + return ( +
+
+ +

{category.label}

+ {list.length} +
+
+ {list.map((s) => ( + + ))} +
+
+ ); + })} +
+ ); +} + +function SkeletonGrid() { + return ( +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+ ); +} + +function EmptyState({ title, body }: { title: string; body: string }) { + return ( +
+

{title}

+

{body}

+
+ ); +} diff --git a/sources/dashboard/src/app/pages/detail-page.tsx b/sources/dashboard/src/app/pages/detail-page.tsx new file mode 100644 index 0000000..ed5aa3e --- /dev/null +++ b/sources/dashboard/src/app/pages/detail-page.tsx @@ -0,0 +1,92 @@ +import { useMemo } from "react"; +import { ArrowLeftIcon } from "@phosphor-icons/react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { LiveChart } from "@/features/metrics/live-chart"; +import { CATEGORIES, formatMetricValue } from "@/lib/metrics"; +import { computeStats } from "@/lib/stats"; +import { useSeries } from "@/stores/metrics-store"; +import { useDetailSeriesKey, useUiActions } from "@/stores/ui-store"; + +export function DetailPage() { + const detailSeriesKey = useDetailSeriesKey(); + const { setView } = useUiActions(); + const series = useSeries()[detailSeriesKey ?? ""]; + + const stats = useMemo(() => (series ? computeStats(series.values) : null), [series]); + + if (!series || !stats) { + return ( +
+

+ This metric is no longer in the live window. +

+ +
+ ); + } + + const category = CATEGORIES.find((c) => c.id === series.category); + const Icon = category?.icon; + const fmt = (v: number) => { + const { value, unit } = formatMetricValue(series.type, v); + return unit ? `${value} ${unit}` : value; + }; + + return ( +
+
+ + / + {category?.label} + / + {series.label} + {series.deviceLabel && ( + · {series.deviceLabel} + )} +
+ +
+ {Icon && } +

{series.label}

+
+ + +
+ +
+
+ +
+ + + + + + +
+

+ Statistics are computed over the live window buffered since connecting. +

+
+ ); +} + +function Stat({ label, value }: { label: string; value: string }) { + return ( + + + {label} + + {value} + + ); +} diff --git a/sources/dashboard/src/app/provider.tsx b/sources/dashboard/src/app/provider.tsx new file mode 100644 index 0000000..b762290 --- /dev/null +++ b/sources/dashboard/src/app/provider.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from "react"; +import { ThemeProvider } from "@/components/theme-provider"; + +export function AppProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/sources/dashboard/src/app/router.tsx b/sources/dashboard/src/app/router.tsx new file mode 100644 index 0000000..4cdfb81 --- /dev/null +++ b/sources/dashboard/src/app/router.tsx @@ -0,0 +1,18 @@ +import { DashboardView } from "@/components/dashboard-view"; +import { DashboardLayout } from "@/components/layout/dashboard-layout"; +import { ConnectionPage } from "@/app/pages/connection-page"; +import { useIsConnected } from "@/stores/connection-store"; + +export function AppRouter() { + const connected = useIsConnected(); + + if (!connected) { + return ; + } + + return ( + + + + ); +} diff --git a/sources/dashboard/src/components/charts/detailed-chart.tsx b/sources/dashboard/src/components/charts/detailed-chart.tsx new file mode 100644 index 0000000..7d48a7b --- /dev/null +++ b/sources/dashboard/src/components/charts/detailed-chart.tsx @@ -0,0 +1,145 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type uPlot from "uplot"; +import { UplotReact, chartTooltipPlugin } from "@/lib/uplot-react"; +import { useMeasuredWidth } from "@/hooks/use-measured-width"; + +function readChartChromeColors() { + const el = document.createElement("span"); + el.style.display = "none"; + document.documentElement.appendChild(el); + + el.style.color = "var(--muted-foreground)"; + const axis = getComputedStyle(el).color; + + el.style.color = "var(--border)"; + const grid = getComputedStyle(el).color; + + el.remove(); + return { axis, grid }; +} + +function useChartChromeColors() { + const [colors, setColors] = useState(readChartChromeColors); + + useEffect(() => { + const update = () => setColors(readChartChromeColors()); + + const observer = new MutationObserver(update); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + const media = window.matchMedia("(prefers-color-scheme: dark)"); + media.addEventListener("change", update); + + return () => { + observer.disconnect(); + media.removeEventListener("change", update); + }; + }, []); + + return colors; +} + +interface DetailedChartProps { + /** Unix epoch milliseconds. */ + times: number[]; + values: number[]; + color: string; + label: string; + formatValue: (value: number) => string; + height?: number; + className?: string; +} + +/** Full axis line chart for a time series. */ +export function DetailedChart({ + times, + values, + color, + label, + formatValue, + height = 260, + className, +}: DetailedChartProps) { + const [ref, width] = useMeasuredWidth(); + const { axis: chartAxis, grid: chartGrid } = useChartChromeColors(); + const zoomedRef = useRef(false); + + const resetScales = useCallback(() => !zoomedRef.current, []); + + const tooltipPlugin = useMemo(() => chartTooltipPlugin(formatValue), [formatValue]); + + const data = useMemo( + () => [times.map((t) => t / 1000), values], + [times, values], + ); + + const baseOptions = useMemo( + () => + ({ + scales: { x: { time: true } }, + legend: { show: false }, + plugins: [tooltipPlugin], + hooks: { + setSelect: [ + (u) => { + if (u.select.width > 0) zoomedRef.current = true; + }, + ], + }, + cursor: { + y: false, + points: { size: 5 }, + bind: { + dblclick: (_u, _targ, handler) => (e) => { + zoomedRef.current = false; + return handler(e); + }, + }, + }, + axes: [ + { + stroke: chartAxis, + grid: { stroke: chartGrid, width: 1 }, + ticks: { stroke: chartGrid, width: 1 }, + font: "11px monospace", + }, + { + stroke: chartAxis, + grid: { stroke: chartGrid, width: 1 }, + ticks: { stroke: chartGrid, width: 1 }, + font: "11px monospace", + size: 60, + values: (_u, splits) => splits.map(formatValue), + }, + ], + series: [ + {}, + { + label, + stroke: color, + width: 1.5, + fill: `${color}1f`, + points: { show: false }, + value: (_u, v) => (v == null ? "—" : formatValue(v)), + }, + ], + }) satisfies Omit, + [chartAxis, chartGrid, color, label, formatValue, tooltipPlugin], + ); + + const options = useMemo( + () => ({ ...baseOptions, width: Math.max(width, 1), height }), + [baseOptions, width, height], + ); + + const ready = times.length > 1 && values.length > 1; + + return ( +
+ {width > 0 && ready && } +
+ ); +} diff --git a/sources/dashboard/src/components/charts/sparkline.tsx b/sources/dashboard/src/components/charts/sparkline.tsx new file mode 100644 index 0000000..7641f58 --- /dev/null +++ b/sources/dashboard/src/components/charts/sparkline.tsx @@ -0,0 +1,41 @@ +import { useMemo } from "react"; +import type uPlot from "uplot"; +import { UplotReact } from "@/lib/uplot-react"; +import { useMeasuredWidth } from "@/hooks/use-measured-width"; + +interface SparklineProps { + values: number[]; + color: string; + height?: number; + className?: string; +} + +/** Minimal axis-less chart */ +export function Sparkline({ values, color, height = 36, className }: SparklineProps) { + const [ref, width] = useMeasuredWidth(); + + const data = useMemo(() => [values.map((_, i) => i), values], [values]); + + const baseOptions = useMemo( + () => + ({ + legend: { show: false }, + cursor: { show: false }, + scales: { x: { time: false } }, + axes: [{ show: false }, { show: false }], + series: [{}, { stroke: color, width: 1.25, fill: `${color}1f`, points: { show: false } }], + }) satisfies Omit, + [color], + ); + + const options = useMemo( + () => ({ ...baseOptions, width: Math.max(width, 1), height }), + [baseOptions, width, height], + ); + + return ( + + ); +} diff --git a/sources/dashboard/src/components/dashboard-view.tsx b/sources/dashboard/src/components/dashboard-view.tsx new file mode 100644 index 0000000..7e58c78 --- /dev/null +++ b/sources/dashboard/src/components/dashboard-view.tsx @@ -0,0 +1,21 @@ +import { Suspense, lazy } from "react"; +import { useView } from "@/stores/ui-store"; + +const DetailPage = lazy(() => + import("@/app/pages/detail-page").then((m) => ({ default: m.DetailPage })), +); + +const DashboardPage = lazy(() => + import("@/app/pages/dashboard-page").then((m) => ({ default: m.DashboardPage })), +); + +export function DashboardView() { + const view = useView(); + + return ( + + {view === "detail" && } + {view === "dashboard" && } + + ); +} diff --git a/sources/dashboard/src/components/layout/dashboard-layout.tsx b/sources/dashboard/src/components/layout/dashboard-layout.tsx new file mode 100644 index 0000000..106f29b --- /dev/null +++ b/sources/dashboard/src/components/layout/dashboard-layout.tsx @@ -0,0 +1,160 @@ +import { + ArrowsClockwiseIcon, + CaretLeftIcon, + ChartLineIcon, + PulseIcon, + SquaresFourIcon, +} from "@phosphor-icons/react"; +import type { ReactNode } from "react"; +import { ModeToggle } from "@/components/mode-toggle"; +import { StatusDot, type StatusTone } from "@/components/status-dot"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { AgentSelector } from "@/features/agents/agent-selector"; +import { useMetricStream } from "@/features/metrics/use-metric-stream"; +import { cn } from "@/lib/utils"; +import { useSelectedAgentId } from "@/stores/agent-store"; +import { useBaseUrl, useConnectionActions } from "@/stores/connection-store"; +import { useStreamStatus, type StreamStatus } from "@/stores/metrics-store"; +import { useSidebarCollapsed, useUiActions, useView, type View } from "@/stores/ui-store"; + +const VIEW_TITLE: Record = { + dashboard: "Live Dashboard", + detail: "Metric Detail", +}; + +const STREAM_LABEL: Record = { + idle: "Idle", + connecting: "Connecting", + open: "Streaming", + reconnecting: "Reconnecting", +}; + +const STREAM_TONE: Record = { + idle: "idle", + connecting: "warn", + open: "online", + reconnecting: "warn", +}; + +function Sidebar() { + const view = useView(); + const collapsed = useSidebarCollapsed(); + const { setView, toggleSidebar } = useUiActions(); + const baseUrl = useBaseUrl(); + const { disconnect } = useConnectionActions(); + const streamStatus = useStreamStatus(); + + return ( + + ); +} + +interface DashboardLayoutProps { + children: ReactNode; +} + +export function DashboardLayout({ children }: DashboardLayoutProps) { + const view = useView(); + const selectedAgentId = useSelectedAgentId(); + const reconnecting = useStreamStatus() === "reconnecting"; + + useMetricStream(); + + return ( +
+ +
+
+

{VIEW_TITLE[view]}

+ {selectedAgentId && ( + + {selectedAgentId} + + )} + {reconnecting && ( + + + Connection lost - retrying… + + )} +
+ +
+
+
{children}
+
+
+ ); +} diff --git a/sources/dashboard/src/components/mode-toggle.tsx b/sources/dashboard/src/components/mode-toggle.tsx new file mode 100644 index 0000000..e0ae794 --- /dev/null +++ b/sources/dashboard/src/components/mode-toggle.tsx @@ -0,0 +1,45 @@ +import { DesktopIcon, MoonIcon, SunIcon } from "@phosphor-icons/react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme } from "@/components/theme-provider"; + +const OPTIONS = [ + { value: "light", label: "Light", icon: SunIcon }, + { value: "dark", label: "Dark", icon: MoonIcon }, + { value: "system", label: "System", icon: DesktopIcon }, +] as const; + +export function ModeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + + + + Toggle theme + + } + /> + + {OPTIONS.map(({ value, label, icon: Icon }) => ( + setTheme(value)} + data-active={theme === value} + > + + {label} + + ))} + + + ); +} diff --git a/sources/dashboard/src/components/status-dot.tsx b/sources/dashboard/src/components/status-dot.tsx new file mode 100644 index 0000000..11d1c17 --- /dev/null +++ b/sources/dashboard/src/components/status-dot.tsx @@ -0,0 +1,34 @@ +import { cn } from "@/lib/utils"; + +export type StatusTone = "online" | "idle" | "warn" | "offline"; + +const TONE_CLASS: Record = { + online: "bg-emerald-400", + idle: "bg-muted-foreground", + warn: "bg-amber-400", + offline: "bg-destructive", +}; + +export function StatusDot({ + tone, + pulse = false, + className, +}: { + tone: StatusTone; + pulse?: boolean; + className?: string; +}) { + return ( + + {pulse && ( + + )} + + + ); +} diff --git a/sources/dashboard/src/components/theme-provider.tsx b/sources/dashboard/src/components/theme-provider.tsx new file mode 100644 index 0000000..414c04d --- /dev/null +++ b/sources/dashboard/src/components/theme-provider.tsx @@ -0,0 +1,71 @@ +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const ThemeProviderContext = createContext(null); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "volta-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setThemeState] = useState( + () => (localStorage.getItem(storageKey) as Theme | null) ?? defaultTheme, + ); + + useEffect(() => { + const root = window.document.documentElement; + + const applyTheme = (resolved: "light" | "dark") => { + root.classList.remove("light", "dark"); + root.classList.add(resolved); + }; + + if (theme === "system") { + const media = window.matchMedia("(prefers-color-scheme: dark)"); + const applySystem = () => applyTheme(media.matches ? "dark" : "light"); + + applySystem(); + media.addEventListener("change", applySystem); + return () => media.removeEventListener("change", applySystem); + } + + applyTheme(theme); + }, [theme]); + + const value: ThemeProviderState = { + theme, + setTheme: (nextTheme) => { + localStorage.setItem(storageKey, nextTheme); + setThemeState(nextTheme); + }, + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeProviderContext); + + if (context === null) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + + return context; +} diff --git a/sources/dashboard/src/components/ui/badge.tsx b/sources/dashboard/src/components/ui/badge.tsx new file mode 100644 index 0000000..0adb8a1 --- /dev/null +++ b/sources/dashboard/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props, + ), + render, + state: { + slot: "badge", + variant, + }, + }); +} + +export { Badge, badgeVariants }; diff --git a/sources/dashboard/src/components/ui/button.tsx b/sources/dashboard/src/components/ui/button.tsx new file mode 100644 index 0000000..9fff92f --- /dev/null +++ b/sources/dashboard/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import { Button as ButtonPrimitive } from "@base-ui/react/button"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-7 rounded-none", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant = "default", + size = "default", + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/sources/dashboard/src/components/ui/card.tsx b/sources/dashboard/src/components/ui/card.tsx new file mode 100644 index 0000000..4ac77a7 --- /dev/null +++ b/sources/dashboard/src/components/ui/card.tsx @@ -0,0 +1,85 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", + className, + )} + {...props} + /> + ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/sources/dashboard/src/components/ui/dropdown-menu.tsx b/sources/dashboard/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..60b0455 --- /dev/null +++ b/sources/dashboard/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,253 @@ +import * as React from "react"; +import { Menu as MenuPrimitive } from "@base-ui/react/menu"; + +import { cn } from "@/lib/utils"; +import { CaretRightIcon, CheckIcon } from "@phosphor-icons/react"; + +function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { + return ; +} + +function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { + return ; +} + +function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { + return ; +} + +function DropdownMenuContent({ + align = "start", + alignOffset = 0, + side = "bottom", + sideOffset = 4, + className, + ...props +}: MenuPrimitive.Popup.Props & + Pick) { + return ( + + + + + + ); +} + +function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { + return ; +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: MenuPrimitive.GroupLabel.Props & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: MenuPrimitive.Item.Props & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: MenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + align = "start", + alignOffset = -3, + side = "right", + sideOffset = 0, + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: MenuPrimitive.CheckboxItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { + return ; +} + +function DropdownMenuRadioItem({ + className, + children, + inset, + ...props +}: MenuPrimitive.RadioItem.Props & { + inset?: boolean; +}) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) { + return ( + + ); +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/sources/dashboard/src/components/ui/input.tsx b/sources/dashboard/src/components/ui/input.tsx new file mode 100644 index 0000000..91a3eb6 --- /dev/null +++ b/sources/dashboard/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { Input as InputPrimitive } from "@base-ui/react/input"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/sources/dashboard/src/components/ui/label.tsx b/sources/dashboard/src/components/ui/label.tsx new file mode 100644 index 0000000..455b1a4 --- /dev/null +++ b/sources/dashboard/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( +