diff --git a/.docker/start_all.sh b/.docker/start_all.sh
deleted file mode 100755
index 4bcdb5e6..00000000
--- a/.docker/start_all.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$DEBUG" = "1" ]; then
- set -x
-fi
-
-if [ "$ALGORAND_DATA" != "/algod/data" ]; then
- echo "Do not override 'ALGORAND_DATA' environment variable."
- exit 1
-fi
-
-/node/run/start_empty.sh &
-/node/run/start_fast_catchup.sh &
-/node/run/start_dev.sh
\ No newline at end of file
diff --git a/.docker/start_dev.sh b/.docker/start_dev.sh
deleted file mode 100755
index 64ff6582..00000000
--- a/.docker/start_dev.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$DEBUG" = "1" ]; then
- set -x
-fi
-
-if [ "$ALGORAND_DATA" != "/algod/data" ]; then
- echo "Do not override 'ALGORAND_DATA' environment variable."
- exit 1
-fi
-
-# Configure the participation node
-if [ -d "$ALGORAND_DATA" ]; then
- if [ -f "$ALGORAND_DATA/genesis.json" ]; then
- if [ "$TOKEN" != "" ]; then
- echo "$TOKEN" >"$EMPTY_DATA/algod.token"
- fi
- if [ "$ADMIN_TOKEN" != "" ]; then
- echo "$ADMIN_TOKEN" >"$EMPTY_DATA/algod.admin.token"
- fi
- algod -o -d "$ALGORAND_DATA" -l "0.0.0.0:8080"
- else
- sed -i "s/NUM_ROUNDS/${NUM_ROUNDS:-30000}/" "/node/run/template.json"
- sed -i "s/\"NetworkName\": \"\"/\"NetworkName\": \"algorun-tui\"/" "/node/run/template.json"
- goal network create --noclean -n tuinet -r "${ALGORAND_DATA}/.." -t "/node/run/template.json"
-
- # Cycle Network
- goal network start -r "${ALGORAND_DATA}/.."
- goal node stop
-
- # Update Tokens
- if [ "$TOKEN" != "" ]; then
- echo "$TOKEN" >"$ALGORAND_DATA/algod.token"
- fi
- if [ "$ADMIN_TOKEN" != "" ]; then
- echo "$ADMIN_TOKEN" >"$ALGORAND_DATA/algod.admin.token"
- fi
- # Import wallet
- goal account import -m "artefact exist coil life turtle edge edge inside punch glance recycle teach melody diet method pause slam dumb race interest amused side learn able heavy"
-
- algod -o -d "$ALGORAND_DATA" -l "0.0.0.0:8080"
- fi
-
-else
- echo "$ALGORAND_DATA" does not exist
- exit 1
-fi
diff --git a/.docker/start_empty.sh b/.docker/start_empty.sh
deleted file mode 100755
index e99ad2ab..00000000
--- a/.docker/start_empty.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$DEBUG" = "1" ]; then
- set -x
-fi
-
-if [ "$ALGORAND_DATA" != "/algod/data" ]; then
- echo "Do not override 'ALGORAND_DATA' environment variable."
- exit 1
-fi
-
-EMPTY_DATA=/algod/empty
-
-# To allow mounting the data directory we need to change permissions
-# to our algorand user. The script is initially run as the root user
-# in order to change permissions, afterwards the script is re-launched
-# as the algorand user.
-if [ "$(id -u)" = '0' ]; then
- chown -R algorand:algorand $EMPTY_DATA
- exec su -p -c "$(readlink -f $0) $@" algorand
-fi
-
-
-# Configure the participation node
-if [ -d "$EMPTY_DATA" ]; then
- if [ "$TOKEN" != "" ]; then
- echo "$TOKEN" > "$EMPTY_DATA/algod.token"
- fi
- if [ "$ADMIN_TOKEN" != "" ]; then
- echo "$ADMIN_TOKEN" > "$EMPTY_DATA/algod.admin.token"
- fi
- cd $EMPTY_DATA
- cp "/node/run/genesis/testnet/genesis.json" genesis.json
- algocfg profile set --yes -d "$EMPTY_DATA" "participation"
- algod -o -d $EMPTY_DATA -l "0.0.0.0:8082"
-else
- echo $EMPTY_DATA does not exist
- exit 1
-fi
\ No newline at end of file
diff --git a/.docker/start_fast_catchup.sh b/.docker/start_fast_catchup.sh
deleted file mode 100755
index 89acc238..00000000
--- a/.docker/start_fast_catchup.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-if [ "$DEBUG" = "1" ]; then
- set -x
-fi
-
-if [ "$ALGORAND_DATA" != "/algod/data" ]; then
- echo "Do not override 'ALGORAND_DATA' environment variable."
- exit 1
-fi
-
-FAST_CATCHUP_DATA=/algod/fast-catchup
-
-# To allow mounting the data directory we need to change permissions
-# to our algorand user. The script is initially run as the root user
-# in order to change permissions, afterwards the script is re-launched
-# as the algorand user.
-if [ "$(id -u)" = '0' ]; then
- chown -R algorand:algorand $FAST_CATCHUP_DATA
- exec su -p -c "$(readlink -f $0) $@" algorand
-fi
-
-function catchup() {
- sleep 5
- goal node catchup --force --min 1000000 -d $FAST_CATCHUP_DATA
-}
-
-# Configure the participation node
-if [ -d "$FAST_CATCHUP_DATA" ]; then
- if [ "$TOKEN" != "" ]; then
- echo "$TOKEN" > "$FAST_CATCHUP_DATA/algod.token"
- fi
- if [ "$ADMIN_TOKEN" != "" ]; then
- echo "$ADMIN_TOKEN" > "$FAST_CATCHUP_DATA/algod.admin.token"
- fi
- cd $FAST_CATCHUP_DATA
- cp "/node/run/genesis/testnet/genesis.json" genesis.json
- algocfg profile set --yes -d "$FAST_CATCHUP_DATA" "participation"
- catchup &
- algod -o -d $FAST_CATCHUP_DATA -l "0.0.0.0:8081"
-else
- echo $FAST_CATCHUP_DATA does not exist
- exit 1
-fi
\ No newline at end of file
diff --git a/.github/workflows/test.yaml b/.github/workflows/code_test.yaml
similarity index 63%
rename from .github/workflows/test.yaml
rename to .github/workflows/code_test.yaml
index 3cad3ebb..92356c93 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/code_test.yaml
@@ -1,4 +1,4 @@
-name: Tests
+name: Code Tests
on:
pull_request:
@@ -16,18 +16,6 @@ jobs:
with:
go-version: 1.22
- - name: setup .algorun.yaml
- run: |
- touch .algorun.yaml
- echo 'server: http://localhost:8080' >> .algorun.yaml
- echo 'token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' >> .algorun.yaml
-
- - name: Start Docker Compose
- run: docker compose up -d
-
- - name: Wait for the server to start
- run: npx wait-on tcp:8080
-
- name: Install dependencies
run: go get .
@@ -53,10 +41,10 @@ jobs:
run: go vet ./...
- name: Build
- run: go build -o bin/algorun *.go
+ run: go build -o bin/nodekit *.go
- - name: Test with the Go CLI
- run: go test ./... -coverprofile=./coverage.txt -covermode=atomic -coverpkg=./...
+ - name: Unit Tests
+ run: make test
- name: Upload results to Codecov
uses: codecov/codecov-action@v4
diff --git a/.github/workflows/node_test.yaml b/.github/workflows/node_test.yaml
new file mode 100644
index 00000000..f4e6fb4c
--- /dev/null
+++ b/.github/workflows/node_test.yaml
@@ -0,0 +1,82 @@
+## This is a temporary flow, until we have our custom docker images that work with systemd for linux.
+## Once we have that, we can remove this and use docker containers in parallel, covering the various OS:es.
+
+name: Node Command OS-Matrix Test
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - "cmd/**"
+
+jobs:
+ ubuntu:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: 1.22
+ - name: Run Ubuntu commands
+ run: |
+ export GOCOVERDIR=$(pwd)/coverage
+ mkdir -p $GOCOVERDIR
+ go build -cover .
+ ./nodekit install
+ systemctl status algorand.service
+ export TOKEN=$(cat /var/lib/algorand/algod.admin.token)
+ curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
+ ./nodekit stop
+ ./nodekit upgrade
+ ./nodekit debug
+ sleep 10
+ ./nodekit catchup
+ ./nodekit catchup debug
+ ./nodekit catchup stop
+ ./nodekit stop
+ ./nodekit uninstall
+ go tool covdata textfmt -i=$GOCOVERDIR -o coverage.txt
+
+ - name: Upload results to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ macos:
+ runs-on: macos-latest
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ run: brew install go
+
+ - name: Run MacOs commands
+ run: |
+ export GOCOVERDIR=$(pwd)/coverage
+ mkdir -p $GOCOVERDIR
+ go build -cover .
+ ./nodekit install
+ sudo launchctl print system/com.algorand.algod
+ sleep 5
+ export TOKEN=$(cat ~/.algorand/algod.admin.token)
+ curl http://localhost:8080/v2/participation -H "X-Algo-API-Token: $TOKEN" | grep "null"
+ ./nodekit stop
+ ./nodekit upgrade
+ ./nodekit debug
+ sleep 10
+ ./nodekit catchup
+ ./nodekit catchup debug
+ ./nodekit catchup stop
+ ./nodekit stop
+ ./nodekit uninstall
+ go tool covdata textfmt -i=$GOCOVERDIR -o coverage.txt
+
+ - name: Upload results to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 3b4a04bd..a08a225b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-algorun-tui
+nodekit
+coverage
bin
.data
@@ -15,6 +16,9 @@ bin
# Test binary, built with `go test -c`
*.test
+# coverage.txt, in case you run tests locally
+coverage.txt
+
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
@@ -32,4 +36,4 @@ go.work.sum
.env
# user-created file
-.algorun.y*ml
+.nodekit.y*ml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 24b1b22d..577ae06b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,13 +7,13 @@ A guide on how to contribute to this project.
Clone the project
```bash
-git clone git@github.com:algorandfoundation/algorun-tui.git
+git clone git@github.com:algorandfoundation/nodekit.git
```
Change to the directory
```bash
-cd algorun-tui
+cd nodekit
```
Build the project
@@ -29,7 +29,7 @@ Optionally, run a sandboxed participation node
docker compose up
```
-Create a configuration file for the participation node in the root directory of the project (.algorun.yaml)
+Create a configuration file for the participation node in the root directory of the project (.nodekit.yaml)
```yaml
algod-endpoint: http://localhost:8080
@@ -40,13 +40,13 @@ Launch the TUI
> [!NOTE]
-> If you skipped the docker container or config file, try running `./bin/algorun` standalone,
+> If you skipped the docker container or config file, try running `./bin/nodekit` standalone,
> which will detect your algorand data directory from the `ALGORAND_DATA` environment variable that works for `goal`.
> Otherwise, provide the `--algod-endpoint` and `--algod-token` arguments so that it can find your node.
-> Note that algorun requires the admin algod token.
+> Note that nodekit requires the admin algod token.
```bash
-./bin/algorun
+./bin/nodekit
```
# ๐ Folder Structure
@@ -67,7 +67,7 @@ All submodules and endpoints **SHOULD** align with the command/ui namespaces.
Example Command:
```bash
-algorun status
+nodekit status
```
Example Structure
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index e6cda073..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,31 +0,0 @@
-FROM golang:1.23-bookworm AS builder
-
-WORKDIR /app
-
-ADD . .
-
-RUN CGO_ENABLED=0 go build -o ./bin/algorun *.go
-
-FROM algorand/algod:latest
-
-ENV TOKEN: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-ENV ADMIN_TOKEN: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-ENV GOSSIP_PORT: 10000
-
-USER root
-
-ADD .docker/start_all.sh /node/run/start_all.sh
-ADD .docker/start_dev.sh /node/run/start_dev.sh
-ADD .docker/start_empty.sh /node/run/start_empty.sh
-ADD .docker/start_fast_catchup.sh /node/run/start_fast_catchup.sh
-
-COPY --from=builder /app/bin/algorun /bin/algorun
-
-RUN apt-get update && apt-get install jq -y
-
-ENTRYPOINT /node/run/start_dev.sh
-CMD []
-
-EXPOSE 8080
-EXPOSE 8081
-EXPOSE 8082
diff --git a/Makefile b/Makefile
index fc3c4d38..82328517 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
build:
- CGO_ENABLED=0 go build -o bin/algorun *.go
+ CGO_ENABLED=0 go build -o bin/nodekit .
test:
- go test -coverpkg=./... -covermode=atomic ./...
+ go test -coverprofile=coverage.out -coverpkg=./... -covermode=atomic ./...
generate:
oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml
diff --git a/README.md b/README.md
index 3f18ad8e..aa63c0c4 100644
--- a/README.md
+++ b/README.md
@@ -1,120 +1,50 @@
-# โจ๏ธ AlgoRun-TUI
+## nodekit
-
-
-
+Manage Algorand nodes from the command line
-
+### Synopsis
----
+
+
+
+
+Manage Algorand nodes from the command line
+
+Overview:
+Welcome to NodeKit, a TUI for managing Algorand nodes.
+A one stop shop for managing Algorand nodes, including node creation, configuration, and management.
+
+Note: This is still a work in progress. Expect bugs and rough edges.
-Terminal UI for managing Algorand nodes.
-Built with [bubbles](https://github.com/charmbracelet/bubbles) & [bubbletea](https://github.com/charmbracelet/bubbletea)
-
-> [!CAUTION]
-> This project is in alpha state and under heavy development. We do not recommend performing actions (e.g. key management) on participation nodes connected to public networks.
-
-# ๐ Get Started
-
-Download the latest release by running
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/algorandfoundation/algorun-tui/refs/heads/main/install.sh | bash
```
-
-Launch the TUI by replacing the `` and ``
-with your server in the following example
-
-> [!IMPORTANT]
-> TUI requires the *admin* token in order to access participation key information. This can be found in the `algod.admin.token` file, e.g. `/var/lib/algorand/algod.admin.token`
-
-```bash
-./algorun --algod-endpoint --algod-token
+nodekit [flags]
```
-# โน๏ธ Advanced Usage
+### Options
-## ๐งโ๐ป Commands
-
-The default command will launch the full TUI application
-
-```bash
-./algorun
```
-
-### Status
-
-Render only the status overview in the terminal
-
-```bash
-./algorun status
+ -d, --datadir string Data directory for the node
+ -h, --help help for nodekit
```
-### Help
-
-Display the usage information for the command
+### SEE ALSO
-```bash
-./algorun help
-```
-## โ๏ธ Configuration
-
-Configuration precedence takes place in the following order:
+* [nodekit bootstrap](/man/nodekit_bootstrap.md) - Initialize a fresh node
+* [nodekit catchup](/man/nodekit_catchup.md) - Manage Fast-Catchup for your node
+* [nodekit configure](/man/nodekit_configure.md) - Change settings on the system (WIP)
+* [nodekit debug](/man/nodekit_debug.md) - Display debugging information
+* [nodekit install](/man/nodekit_install.md) - Install the node daemon
+* [nodekit start](/man/nodekit_start.md) - Start the node daemon
+* [nodekit stop](/man/nodekit_stop.md) - Stop the node daemon
+* [nodekit uninstall](/man/nodekit_uninstall.md) - Uninstall the node daemon
+* [nodekit upgrade](/man/nodekit_upgrade.md) - Upgrade the node daemon
-1. [ALGORAND_DATA Parsing](#algorand_data)
-2. [Configuration File](#configuration-file)
-3. [Environment Variables](#environment-variables)
-4. [Command Line Flag Arguments](#flags)
+###### Auto generated by spf13/cobra on 7-Jan-2025
-### Flags
+### Installing
-The application supports the `algod-endpoint` and `algod-token` flags for configuration.
+Connect to your server and run the installation script which will bootstrap your node.
```bash
-./algorun --algod-endpoint http://localhost:8080 --algod-token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-```
-
-### Configuration File
-
-The configuration file is named `.algorun.yaml` and is loaded in the following order:
-
-1. Current Directory
-2. Home Directory
-3. /etc/algorun/
-
-Example `.algorun.yaml` configuration file:
-
-```yaml
-algod-endpoint: "http://localhost:8080"
-algod-token: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-```
-
-### Environment Variables
-
-Environment variables can be set in order to override a configuration or ALGORAND_DATA setting
-but cannot be used to override the command line arguments.
-
-The following are the additional ENV variables the TUI supports
-
-| Name | Example |
-|------------------------|----------------------------------------------------------------------------------------|
-| ALGORUN_ALGOD-ENDPOINT | ALGORUN_ALGOD-ENDPOINT="http://localhost:8080" |
-| ALGORUN_ALGOD-TOKEN | ALGORUN_ALGOD-TOKEN="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" |
-
-### ALGORAND_DATA
-
-The TUI searches the environment for an `ALGORAND_DATA` variable.
-It then loads the `algod-token` and `algod-endpoint` values from
-the algod data directory.
-
+curl -fsSL https://raw.githubusercontent.com/algorandfoundation/nodekit/refs/heads/main/install.sh | bash
+```
\ No newline at end of file
diff --git a/VISION.md b/VISION.md
index baa4abd4..2a63413d 100644
--- a/VISION.md
+++ b/VISION.md
@@ -6,7 +6,7 @@ is largely inspired by it.
# Motivation
-**Staking Rewardsโข๏ธ** require additional configuration and monitoring which this **Hacktui** seeks to simplify.
+**Staking Rewardsโข๏ธ** require additional configuration and monitoring which this **NodeKit** seeks to simplify.
## Primary Goals
diff --git a/api/catchpoint.go b/api/catchpoint.go
new file mode 100644
index 00000000..5ef3168f
--- /dev/null
+++ b/api/catchpoint.go
@@ -0,0 +1,73 @@
+package api
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "strings"
+)
+
+type CatchPointUrl string
+
+const (
+ FNet CatchPointUrl = "https://fnet-catchpoints.algorand.green/latest"
+ BetaNet CatchPointUrl = "https://algorand-catchpoints.s3.us-east-2.amazonaws.com/channel/betanet/latest.catchpoint"
+ TestNet CatchPointUrl = "https://algorand-catchpoints.s3.us-east-2.amazonaws.com/channel/testnet/latest.catchpoint"
+ MainNet CatchPointUrl = "https://algorand-catchpoints.s3.us-east-2.amazonaws.com/channel/mainnet/latest.catchpoint"
+)
+
+const InvalidNetworkParamMsg = "invalid network"
+
+type LatestCatchpointResponse struct {
+ HTTPResponse *http.Response
+ ResponseCode int
+ ResponseStatus string
+ JSON200 string
+}
+
+func (r LatestCatchpointResponse) StatusCode() int {
+ return r.ResponseCode
+}
+func (r LatestCatchpointResponse) Status() string {
+ return r.ResponseStatus
+}
+
+func GetLatestCatchpointWithResponse(http HttpPkgInterface, network string) (LatestCatchpointResponse, error) {
+ var response LatestCatchpointResponse
+
+ var url CatchPointUrl
+ switch network {
+ case "fnet-v1", "fnet":
+ url = FNet
+ case "betanet-v1.0", "betanet":
+ url = BetaNet
+ case "testnet-v1.0", "testnet":
+ url = TestNet
+ case "mainnet-v1.0", "mainnet":
+ url = MainNet
+ }
+ if url == "" {
+ return response, errors.New(InvalidNetworkParamMsg)
+ }
+ res, err := http.Get(string(url))
+ response.HTTPResponse = res
+ if err != nil {
+ return response, err
+ }
+ defer res.Body.Close()
+
+ // Handle invalid codes as errors
+ if res.StatusCode >= 300 {
+ return response, errors.New(res.Status)
+ }
+
+ // Read from the response
+ var body []byte
+ body, err = io.ReadAll(res.Body)
+ if err != nil {
+ return response, err
+ }
+ // Set the body and return
+ response.JSON200 = strings.Replace(string(body), "\n", "", -1)
+ return response, nil
+}
diff --git a/api/catchpoint_test.go b/api/catchpoint_test.go
new file mode 100644
index 00000000..ff070204
--- /dev/null
+++ b/api/catchpoint_test.go
@@ -0,0 +1,13 @@
+package api
+
+import (
+ "testing"
+)
+
+func Test_GetLatestCatchpoint(t *testing.T) {
+ catchpoint, err := GetLatestCatchpointWithResponse(new(HttpPkg), "mainnet")
+ if err != nil {
+ t.Error(err)
+ }
+ t.Log(catchpoint)
+}
diff --git a/api/genesis.go b/api/genesis.go
new file mode 100644
index 00000000..3ada872a
--- /dev/null
+++ b/api/genesis.go
@@ -0,0 +1,42 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+)
+
+type GenesisFileKey string
+
+const (
+ MainnetGenesisKey GenesisFileKey = "mainnet"
+ TestnetGenesisKey GenesisFileKey = "testnet"
+ FnetGenesisKey GenesisFileKey = "fnet"
+)
+
+type GenesisFileResponse struct {
+ ResponseCode int
+ ResponseStatus string
+ JSON200 string
+}
+
+func (r GenesisFileResponse) StatusCode() int {
+ return r.ResponseCode
+}
+func (r GenesisFileResponse) Status() string {
+ return r.ResponseStatus
+}
+func GetGenesis(key GenesisFileKey) {
+ var url string
+ if key == FnetGenesisKey {
+ url = "http://relay-eu-no-1.algorand.green:8184/genesis"
+ } else {
+ url = fmt.Sprintf("https://raw.githubusercontent.com/algorand/go-algorand/master/installer/genesis/%s/genesis.json", key)
+ }
+ resp, err := http.Get(url)
+ if err != nil {
+ panic(err)
+ }
+ defer resp.Body.Close()
+ fmt.Println(resp.StatusCode)
+
+}
diff --git a/api/github.go b/api/github.go
new file mode 100644
index 00000000..89292f06
--- /dev/null
+++ b/api/github.go
@@ -0,0 +1,68 @@
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+)
+
+const ChannelNotFoundMsg = "channel not found"
+
+type GithubVersionResponse struct {
+ HTTPResponse *http.Response
+ ResponseCode int
+ ResponseStatus string
+ JSON200 string
+}
+
+func (r GithubVersionResponse) StatusCode() int {
+ return r.ResponseCode
+}
+func (r GithubVersionResponse) Status() string {
+ return r.ResponseStatus
+}
+
+func GetGoAlgorandReleaseWithResponse(http HttpPkgInterface, channel string) (*GithubVersionResponse, error) {
+ var versions GithubVersionResponse
+ resp, err := http.Get("https://api.github.com/repos/algorand/go-algorand/releases")
+ versions.HTTPResponse = resp
+ if resp == nil || err != nil {
+ return nil, err
+ }
+ // Update Model
+ versions.ResponseCode = resp.StatusCode
+ versions.ResponseStatus = resp.Status
+
+ // Exit if not 200
+ if resp.StatusCode != 200 {
+ return &versions, nil
+ }
+
+ defer resp.Body.Close()
+
+ // Parse the versions to a map
+ var versionsMap []map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&versionsMap); err != nil {
+ return &versions, err
+ }
+ // Look for versions in the response
+ var versionResponse *string
+ for i := range versionsMap {
+ tn := versionsMap[i]["tag_name"].(string)
+ if strings.Contains(tn, channel) {
+ versionResponse = &tn
+ break
+ }
+
+ }
+
+ // If the tag was not found, return an error
+ if versionResponse == nil {
+ return &versions, errors.New(ChannelNotFoundMsg)
+ }
+
+ // Update the JSON200 data and return
+ versions.JSON200 = *versionResponse
+ return &versions, nil
+}
diff --git a/api/github_test.go b/api/github_test.go
new file mode 100644
index 00000000..abfba74c
--- /dev/null
+++ b/api/github_test.go
@@ -0,0 +1,54 @@
+package api
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/http"
+ "testing"
+)
+
+type testResponse struct {
+ HttpPkgInterface
+}
+
+var jsonStr = `[{
+ "tag_name": "v3.26.0-beta"
+ }]`
+
+func (testResponse) Get(url string) (resp *http.Response, err error) {
+
+ responseBody := io.NopCloser(bytes.NewReader([]byte(jsonStr)))
+ return &http.Response{
+ StatusCode: 200,
+ Body: responseBody,
+ }, nil
+}
+
+type testError struct {
+ HttpPkgInterface
+}
+
+func (testError) Get(url string) (resp *http.Response, err error) {
+ return &http.Response{
+ StatusCode: 404,
+ }, errors.New("not found")
+}
+
+func Test_Github(t *testing.T) {
+ r, err := GetGoAlgorandReleaseWithResponse(new(testResponse), "beta")
+ if err != nil {
+ t.Error(err)
+ }
+ if r.StatusCode() != 200 {
+ t.Error("should be 200 response")
+ }
+ if r.JSON200 != "v3.26.0-beta" {
+ t.Error("should return v3.26.0-beta")
+ }
+
+ _, err = GetGoAlgorandReleaseWithResponse(new(testError), "beta")
+ if err == nil {
+ t.Error("should fail to get")
+ }
+}
diff --git a/api/http.go b/api/http.go
new file mode 100644
index 00000000..7feb4ad0
--- /dev/null
+++ b/api/http.go
@@ -0,0 +1,24 @@
+package api
+
+import (
+ "io"
+ "net/http"
+)
+
+type HttpPkg struct {
+ HttpPkgInterface
+}
+
+func (HttpPkg) Get(url string) (resp *http.Response, err error) {
+ return http.Get(url)
+}
+func (HttpPkg) Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) {
+ return http.Post(url, contentType, body)
+}
+
+var Http HttpPkg
+
+type HttpPkgInterface interface {
+ Get(url string) (resp *http.Response, err error)
+ Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
+}
diff --git a/api/lf.go b/api/lf.go
index 90a18c3e..e5ba8068 100644
--- a/api/lf.go
+++ b/api/lf.go
@@ -574,6 +574,12 @@ type GetBlockParams struct {
// GetBlockParamsFormat defines parameters for GetBlock.
type GetBlockParamsFormat string
+// StartCatchupParams defines parameters for StartCatchup.
+type StartCatchupParams struct {
+ // Min Specify the minimum number of blocks which the ledger must be advanced by in order to start the catchup. This is useful for simplifying tools which support fast catchup, they can run the catchup unconditionally and the node will skip the catchup if it is not needed.
+ Min *int `form:"min,omitempty" json:"min,omitempty"`
+}
+
// GenerateParticipationKeysParams defines parameters for GenerateParticipationKeys.
type GenerateParticipationKeysParams struct {
// Dilution Key dilution for two-level participation keys (defaults to sqrt of validity window).
@@ -671,6 +677,12 @@ type ClientInterface interface {
// GetBlock request
GetBlock(ctx context.Context, round int, params *GetBlockParams, reqEditors ...RequestEditorFn) (*http.Response, error)
+ // AbortCatchup request
+ AbortCatchup(ctx context.Context, catchpoint string, reqEditors ...RequestEditorFn) (*http.Response, error)
+
+ // StartCatchup request
+ StartCatchup(ctx context.Context, catchpoint string, params *StartCatchupParams, reqEditors ...RequestEditorFn) (*http.Response, error)
+
// GetParticipationKeys request
GetParticipationKeys(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -747,6 +759,30 @@ func (c *Algod) GetBlock(ctx context.Context, round int, params *GetBlockParams,
return c.Client.Do(req)
}
+func (c *Algod) AbortCatchup(ctx context.Context, catchpoint string, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewAbortCatchupRequest(c.Server, catchpoint)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
+func (c *Algod) StartCatchup(ctx context.Context, catchpoint string, params *StartCatchupParams, reqEditors ...RequestEditorFn) (*http.Response, error) {
+ req, err := NewStartCatchupRequest(c.Server, catchpoint, params)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(ctx)
+ if err := c.applyEditors(ctx, req, reqEditors); err != nil {
+ return nil, err
+ }
+ return c.Client.Do(req)
+}
+
func (c *Algod) GetParticipationKeys(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewGetParticipationKeysRequest(c.Server)
if err != nil {
@@ -1037,6 +1073,96 @@ func NewGetBlockRequest(server string, round int, params *GetBlockParams) (*http
return req, nil
}
+// NewAbortCatchupRequest generates requests for AbortCatchup
+func NewAbortCatchupRequest(server string, catchpoint string) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "catchpoint", runtime.ParamLocationPath, catchpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/v2/catchup/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("DELETE", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
+// NewStartCatchupRequest generates requests for StartCatchup
+func NewStartCatchupRequest(server string, catchpoint string, params *StartCatchupParams) (*http.Request, error) {
+ var err error
+
+ var pathParam0 string
+
+ pathParam0, err = runtime.StyleParamWithLocation("simple", false, "catchpoint", runtime.ParamLocationPath, catchpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ serverURL, err := url.Parse(server)
+ if err != nil {
+ return nil, err
+ }
+
+ operationPath := fmt.Sprintf("/v2/catchup/%s", pathParam0)
+ if operationPath[0] == '/' {
+ operationPath = "." + operationPath
+ }
+
+ queryURL, err := serverURL.Parse(operationPath)
+ if err != nil {
+ return nil, err
+ }
+
+ if params != nil {
+ queryValues := queryURL.Query()
+
+ if params.Min != nil {
+
+ if queryFrag, err := runtime.StyleParamWithLocation("form", true, "min", runtime.ParamLocationQuery, *params.Min); err != nil {
+ return nil, err
+ } else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+ return nil, err
+ } else {
+ for k, v := range parsed {
+ for _, v2 := range v {
+ queryValues.Add(k, v2)
+ }
+ }
+ }
+
+ }
+
+ queryURL.RawQuery = queryValues.Encode()
+ }
+
+ req, err := http.NewRequest("POST", queryURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return req, nil
+}
+
// NewGetParticipationKeysRequest generates requests for GetParticipationKeys
func NewGetParticipationKeysRequest(server string) (*http.Request, error) {
var err error
@@ -1420,6 +1546,12 @@ type ClientWithResponsesInterface interface {
// GetBlockWithResponse request
GetBlockWithResponse(ctx context.Context, round int, params *GetBlockParams, reqEditors ...RequestEditorFn) (*GetBlockResponse, error)
+ // AbortCatchupWithResponse request
+ AbortCatchupWithResponse(ctx context.Context, catchpoint string, reqEditors ...RequestEditorFn) (*AbortCatchupResponse, error)
+
+ // StartCatchupWithResponse request
+ StartCatchupWithResponse(ctx context.Context, catchpoint string, params *StartCatchupParams, reqEditors ...RequestEditorFn) (*StartCatchupResponse, error)
+
// GetParticipationKeysWithResponse request
GetParticipationKeysWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetParticipationKeysResponse, error)
@@ -1548,6 +1680,67 @@ func (r GetBlockResponse) StatusCode() int {
return 0
}
+type AbortCatchupResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *struct {
+ // CatchupMessage Catchup abort response string
+ CatchupMessage string `json:"catchup-message"`
+ }
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r AbortCatchupResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r AbortCatchupResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
+type StartCatchupResponse struct {
+ Body []byte
+ HTTPResponse *http.Response
+ JSON200 *struct {
+ // CatchupMessage Catchup start response string
+ CatchupMessage string `json:"catchup-message"`
+ }
+ JSON201 *struct {
+ // CatchupMessage Catchup start response string
+ CatchupMessage string `json:"catchup-message"`
+ }
+ JSON400 *ErrorResponse
+ JSON401 *ErrorResponse
+ JSON408 *ErrorResponse
+ JSON500 *ErrorResponse
+}
+
+// Status returns HTTPResponse.Status
+func (r StartCatchupResponse) Status() string {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.Status
+ }
+ return http.StatusText(0)
+}
+
+// StatusCode returns HTTPResponse.StatusCode
+func (r StartCatchupResponse) StatusCode() int {
+ if r.HTTPResponse != nil {
+ return r.HTTPResponse.StatusCode
+ }
+ return 0
+}
+
type GetParticipationKeysResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -1971,6 +2164,24 @@ func (c *ClientWithResponses) GetBlockWithResponse(ctx context.Context, round in
return ParseGetBlockResponse(rsp)
}
+// AbortCatchupWithResponse request returning *AbortCatchupResponse
+func (c *ClientWithResponses) AbortCatchupWithResponse(ctx context.Context, catchpoint string, reqEditors ...RequestEditorFn) (*AbortCatchupResponse, error) {
+ rsp, err := c.AbortCatchup(ctx, catchpoint, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseAbortCatchupResponse(rsp)
+}
+
+// StartCatchupWithResponse request returning *StartCatchupResponse
+func (c *ClientWithResponses) StartCatchupWithResponse(ctx context.Context, catchpoint string, params *StartCatchupParams, reqEditors ...RequestEditorFn) (*StartCatchupResponse, error) {
+ rsp, err := c.StartCatchup(ctx, catchpoint, params, reqEditors...)
+ if err != nil {
+ return nil, err
+ }
+ return ParseStartCatchupResponse(rsp)
+}
+
// GetParticipationKeysWithResponse request returning *GetParticipationKeysResponse
func (c *ClientWithResponses) GetParticipationKeysWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetParticipationKeysResponse, error) {
rsp, err := c.GetParticipationKeys(ctx, reqEditors...)
@@ -2228,6 +2439,123 @@ func ParseGetBlockResponse(rsp *http.Response) (*GetBlockResponse, error) {
return response, nil
}
+// ParseAbortCatchupResponse parses an HTTP response from a AbortCatchupWithResponse call
+func ParseAbortCatchupResponse(rsp *http.Response) (*AbortCatchupResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &AbortCatchupResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest struct {
+ // CatchupMessage Catchup abort response string
+ CatchupMessage string `json:"catchup-message"`
+ }
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
+// ParseStartCatchupResponse parses an HTTP response from a StartCatchupWithResponse call
+func ParseStartCatchupResponse(rsp *http.Response) (*StartCatchupResponse, error) {
+ bodyBytes, err := io.ReadAll(rsp.Body)
+ defer func() { _ = rsp.Body.Close() }()
+ if err != nil {
+ return nil, err
+ }
+
+ response := &StartCatchupResponse{
+ Body: bodyBytes,
+ HTTPResponse: rsp,
+ }
+
+ switch {
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
+ var dest struct {
+ // CatchupMessage Catchup start response string
+ CatchupMessage string `json:"catchup-message"`
+ }
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON200 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201:
+ var dest struct {
+ // CatchupMessage Catchup start response string
+ CatchupMessage string `json:"catchup-message"`
+ }
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON201 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON400 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON401 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 408:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON408 = &dest
+
+ case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500:
+ var dest ErrorResponse
+ if err := json.Unmarshal(bodyBytes, &dest); err != nil {
+ return nil, err
+ }
+ response.JSON500 = &dest
+
+ }
+
+ return response, nil
+}
+
// ParseGetParticipationKeysResponse parses an HTTP response from a GetParticipationKeysWithResponse call
func ParseGetParticipationKeysResponse(rsp *http.Response) (*GetParticipationKeysResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
diff --git a/api/response.go b/api/response.go
new file mode 100644
index 00000000..2cadca28
--- /dev/null
+++ b/api/response.go
@@ -0,0 +1,6 @@
+package api
+
+type ResponseInterface interface {
+ StatusCode() int
+ Status() string
+}
diff --git a/api/status.go b/api/status.go
new file mode 100644
index 00000000..a3ae728d
--- /dev/null
+++ b/api/status.go
@@ -0,0 +1,30 @@
+package api
+
+type StatusLike struct {
+ Catchpoint *string `json:"catchpoint,omitempty"`
+ CatchpointAcquiredBlocks *int `json:"catchpoint-acquired-blocks,omitempty"`
+ CatchpointProcessedAccounts *int `json:"catchpoint-processed-accounts,omitempty"`
+ CatchpointProcessedKvs *int `json:"catchpoint-processed-kvs,omitempty"`
+ CatchpointTotalAccounts *int `json:"catchpoint-total-accounts,omitempty"`
+ CatchpointTotalBlocks *int `json:"catchpoint-total-blocks,omitempty"`
+ CatchpointTotalKvs *int `json:"catchpoint-total-kvs,omitempty"`
+ CatchpointVerifiedAccounts *int `json:"catchpoint-verified-accounts,omitempty"`
+ CatchpointVerifiedKvs *int `json:"catchpoint-verified-kvs,omitempty"`
+ CatchupTime int `json:"catchup-time"`
+ LastCatchpoint *string `json:"last-catchpoint,omitempty"`
+ LastRound int `json:"last-round"`
+ LastVersion string `json:"last-version"`
+ NextVersion string `json:"next-version"`
+ NextVersionRound int `json:"next-version-round"`
+ NextVersionSupported bool `json:"next-version-supported"`
+ StoppedAtUnsupportedRound bool `json:"stopped-at-unsupported-round"`
+ TimeSinceLastRound int `json:"time-since-last-round"`
+ UpgradeDelay *int `json:"upgrade-delay,omitempty"`
+ UpgradeNextProtocolVoteBefore *int `json:"upgrade-next-protocol-vote-before,omitempty"`
+ UpgradeNoVotes *int `json:"upgrade-no-votes,omitempty"`
+ UpgradeNodeVote *bool `json:"upgrade-node-vote,omitempty"`
+ UpgradeVoteRounds *int `json:"upgrade-vote-rounds,omitempty"`
+ UpgradeVotes *int `json:"upgrade-votes,omitempty"`
+ UpgradeVotesRequired *int `json:"upgrade-votes-required,omitempty"`
+ UpgradeYesVotes *int `json:"upgrade-yes-votes,omitempty"`
+}
diff --git a/assets/Banner.gif b/assets/Banner.gif
deleted file mode 100644
index b4c9bd23..00000000
Binary files a/assets/Banner.gif and /dev/null differ
diff --git a/assets/TUI.png b/assets/TUI.png
deleted file mode 100644
index 76f3e21d..00000000
Binary files a/assets/TUI.png and /dev/null differ
diff --git a/assets/footer.md b/assets/footer.md
new file mode 100644
index 00000000..d6d3bfee
--- /dev/null
+++ b/assets/footer.md
@@ -0,0 +1,7 @@
+### Installing
+
+Connect to your server and run the installation script which will bootstrap your node.
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/algorandfoundation/nodekit/refs/heads/main/install.sh | bash
+```
\ No newline at end of file
diff --git a/assets/nodekit.png b/assets/nodekit.png
new file mode 100644
index 00000000..bcf48d09
Binary files /dev/null and b/assets/nodekit.png differ
diff --git a/cmd/bootstrap.go b/cmd/bootstrap.go
new file mode 100644
index 00000000..a9bcac02
--- /dev/null
+++ b/cmd/bootstrap.go
@@ -0,0 +1,173 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/api"
+ cmdutils "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/algorandfoundation/nodekit/ui"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/bootstrap"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+ "time"
+)
+
+// bootstrapCmdShort provides a brief description of the "bootstrap" command to initialize a fresh Algorand node.
+var bootstrapCmdShort = "Initialize a fresh node"
+
+// bootstrapCmdLong provides a detailed description of the "bootstrap" command, including its purpose and functionality.
+var bootstrapCmdLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.BANNER,
+ "",
+ style.Bold(bootstrapCmdShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Get up and running with a fresh Algorand node.",
+ "Uses the local package manager to install Algorand, and then starts the node and preforms a Fast-Catchup.",
+ "",
+ style.Yellow.Render("Note: This command only supports the default data directory, /var/lib/algorand"),
+)
+
+var tutorial = `# Welcome!
+
+This is the beginning of your adventure into running the an Algorand node!
+
+`
+
+// bootstrapCmd defines the "debug" command used to display diagnostic information for developers, including debug data.
+var bootstrapCmd = &cobra.Command{
+ Use: "bootstrap",
+ Short: bootstrapCmdShort,
+ Long: bootstrapCmdLong,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := context.Background()
+ httpPkg := new(api.HttpPkg)
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithAutoStyle(),
+ )
+ fmt.Print(style.Purple(style.BANNER))
+ out, err := r.Render(tutorial)
+ if err != nil {
+ return err
+ }
+ fmt.Println(out)
+
+ model := bootstrap.NewModel()
+ p := tea.NewProgram(model)
+ var msg *app.BootstrapMsg
+ go func() {
+ for {
+ val := <-model.Outside
+ switch val.(type) {
+ case app.BootstrapMsg:
+ msgVal := val.(app.BootstrapMsg)
+ msg = &msgVal
+ }
+ }
+ }()
+
+ if _, err := p.Run(); err != nil {
+ log.Fatal(err)
+ }
+ if msg == nil {
+ return nil
+ }
+
+ log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
+ if msg.Install && !algod.IsInstalled() {
+ err := algod.Install()
+ if err != nil {
+ return err
+ }
+ }
+
+ // Wait for algod
+ time.Sleep(10 * time.Second)
+
+ if !algod.IsRunning() {
+ log.Fatal("algod is not running")
+ }
+
+ dataDir, err := algod.GetDataDir("")
+ if err != nil {
+ return err
+ }
+ // Create the client
+ client, err := algod.GetClient(dataDir)
+ if err != nil {
+ return err
+ }
+
+ if msg.Catchup {
+
+ network, err := utils.GetNetworkFromDataDir(dataDir)
+ if err != nil {
+ return err
+ }
+ // Get the latest catchpoint
+ catchpoint, _, err := algod.GetLatestCatchpoint(httpPkg, network)
+ if err != nil && err.Error() == api.InvalidNetworkParamMsg {
+ log.Fatal("This network does not support fast-catchup.")
+ } else {
+ log.Info(style.Green.Render("Latest Catchpoint: " + catchpoint))
+ }
+
+ // Start catchup
+ res, _, err := algod.StartCatchup(ctx, client, catchpoint, nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Info(style.Green.Render(res))
+
+ }
+
+ t := new(system.Clock)
+ // Fetch the state and handle any creation errors
+ state, stateResponse, err := algod.NewStateModel(ctx, client, httpPkg)
+ cmdutils.WithInvalidResponsesExplanations(err, stateResponse, cmd.UsageString())
+ cobra.CheckErr(err)
+
+ // Construct the TUI Model from the State
+ m, err := ui.NewViewportViewModel(state, client)
+ cobra.CheckErr(err)
+
+ // Construct the TUI Application
+ p = tea.NewProgram(
+ m,
+ tea.WithAltScreen(),
+ tea.WithFPS(120),
+ )
+
+ // Watch for State Updates on a separate thread
+ // TODO: refactor into context aware watcher without callbacks
+ go func() {
+ state.Watch(func(status *algod.StateModel, err error) {
+ if err == nil {
+ p.Send(state)
+ }
+ if err != nil {
+ p.Send(state)
+ p.Send(err)
+ }
+ }, ctx, t)
+ }()
+
+ // Execute the TUI Application
+ _, err = p.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+ return nil
+ },
+}
diff --git a/cmd/catchup/catchup.go b/cmd/catchup/catchup.go
new file mode 100644
index 00000000..8ebd2de8
--- /dev/null
+++ b/cmd/catchup/catchup.go
@@ -0,0 +1,76 @@
+package catchup
+
+import (
+ "context"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+var (
+ // dataDir path to the algorand data folder
+ dataDir string = ""
+
+ // defaultLag represents the default minimum catchup delay in milliseconds for the Fast Catchup process.
+ defaultLag int = 30_000
+
+ // cmdLong provides a detailed description of the Fast-Catchup feature, explaining its purpose and expected sync durations.
+ cmdLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold("Fast-Catchup is a feature that allows your node to catch up to the network faster than normal."),
+ "",
+ style.BoldUnderline("Overview:"),
+ "The entire process should sync a node in minutes rather than hours or days.",
+ "Actual sync times may vary depending on the number of accounts, number of blocks and the network.",
+ "",
+ style.Yellow.Render("Note: Not all networks support Fast-Catchup."),
+ )
+
+ // Cmd represents the root command for managing an Algorand node, including its description and usage instructions.
+ Cmd = utils.WithAlgodFlags(&cobra.Command{
+ Use: "catchup",
+ Short: "Manage Fast-Catchup for your node",
+ Long: cmdLong,
+ Run: func(cmd *cobra.Command, args []string) {
+ // Create Clients
+ ctx := context.Background()
+ httpPkg := new(api.HttpPkg)
+ client, err := algod.GetClient(dataDir)
+ cobra.CheckErr(err)
+
+ // Fetch Status from Node
+ status, response, err := algod.NewStatus(ctx, client, httpPkg)
+ utils.WithInvalidResponsesExplanations(err, response, cmd.UsageString())
+ if status.State == algod.FastCatchupState {
+ log.Fatal(style.Red.Render("Node is currently catching up"))
+ }
+
+ // Get the Latest Catchpoint
+ catchpoint, _, err := algod.GetLatestCatchpoint(httpPkg, status.Network)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Info(style.Green.Render("Latest Catchpoint: " + catchpoint))
+
+ // Submit the Catchpoint to the Algod Node, using the StartCatchupParams to skip
+ res, _, err := algod.StartCatchup(ctx, client, catchpoint, &api.StartCatchupParams{Min: &defaultLag})
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Info(style.Green.Render(res))
+ },
+ }, &dataDir)
+)
+
+func init() {
+ Cmd.AddCommand(startCmd)
+ Cmd.AddCommand(stopCmd)
+ Cmd.AddCommand(debugCmd)
+}
diff --git a/cmd/catchup/debug.go b/cmd/catchup/debug.go
new file mode 100644
index 00000000..c15a4f4f
--- /dev/null
+++ b/cmd/catchup/debug.go
@@ -0,0 +1,94 @@
+package catchup
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+type Catchpoint struct {
+ // IsSupported indicates whether fast-catchup is supported
+ IsSupported bool `json:"supported"`
+
+ // IsRunning indicates whether the fast-catchup process is running
+ IsRunning bool `json:"running"`
+
+ // LatestCatchpoint holds the most recent catchpoint identifier captured by the service, if available.
+ LatestCatchpoint *string `json:"latest"`
+
+ // CatchpointScore scores the node based on how well it can preform a catchup
+ CatchpointScore int `json:"score"`
+}
+
+// DebugInfo represents the debugging information of the catchpoint service.
+type DebugInfo struct {
+ Status algod.Status `json:"status"`
+ Catchpoint `json:"catchpoint"`
+}
+
+// debugCmdShort provides a concise description for the debug command, indicating it displays debug information for Fast-Catchup.
+var debugCmdShort = "Display debug information for Fast-Catchup."
+
+// debugCmdLong provides a detailed description for the debug command, focusing on debugging fast-catchup issues.
+var debugCmdLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(debugCmdShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "This information is useful for debugging fast-catchup issues.",
+ "",
+ style.Yellow.Render("Note: Not all networks support Fast-Catchup."),
+)
+
+// debugCmd is a Cobra command used to check the node's sync status and initiate a fast catchup when necessary.
+var debugCmd = utils.WithAlgodFlags(&cobra.Command{
+ Use: "debug",
+ Short: debugCmdShort,
+ Long: debugCmdLong,
+ SilenceUsage: false,
+ Run: func(cmd *cobra.Command, args []string) {
+ ctx := context.Background()
+ httpPkg := new(api.HttpPkg)
+ client, err := algod.GetClient(dataDir)
+ cobra.CheckErr(err)
+
+ status, response, err := algod.NewStatus(ctx, client, httpPkg)
+ utils.WithInvalidResponsesExplanations(err, response, cmd.UsageString())
+
+ var isSupported bool
+ catchpoint, _, err := algod.GetLatestCatchpoint(httpPkg, status.Network)
+ if err != nil && err.Error() == api.InvalidNetworkParamMsg {
+ isSupported = false
+ } else {
+ isSupported = true
+ }
+
+ info := DebugInfo{
+ Status: status,
+ Catchpoint: Catchpoint{
+ IsRunning: status.State == algod.FastCatchupState,
+ IsSupported: isSupported,
+ LatestCatchpoint: &catchpoint,
+ CatchpointScore: 0,
+ },
+ }
+
+ data, err := json.MarshalIndent(info, "", " ")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Info(style.Blue.Render("Copy and paste the following to a bug report:"))
+ fmt.Println(style.Bold(string(data)))
+
+ },
+}, &dataDir)
diff --git a/cmd/catchup/start.go b/cmd/catchup/start.go
new file mode 100644
index 00000000..b6febf2b
--- /dev/null
+++ b/cmd/catchup/start.go
@@ -0,0 +1,63 @@
+package catchup
+
+import (
+ "context"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+// startCmdLong provides a detailed description and overview message for the 'start' command, including notes and caveats.
+var startCmdLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold("Catchup the node to the latest catchpoint."),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Starting a catchup will sync the node to the latest catchpoint.",
+ "Actual sync times may vary depending on the number of accounts, number of blocks and the network.",
+ "",
+ style.Yellow.Render("Note: Not all networks support Fast-Catchup."),
+)
+
+// startCmd is a Cobra command used to check the node's sync status and initiate a fast catchup when necessary.
+var startCmd = utils.WithAlgodFlags(&cobra.Command{
+ Use: "start",
+ Short: "Get the latest catchpoint and start catching up.",
+ Long: startCmdLong,
+ SilenceUsage: true,
+ Run: func(cmd *cobra.Command, args []string) {
+ ctx := context.Background()
+ httpPkg := new(api.HttpPkg)
+ client, err := algod.GetClient(dataDir)
+ cobra.CheckErr(err)
+
+ status, response, err := algod.NewStatus(ctx, client, httpPkg)
+ utils.WithInvalidResponsesExplanations(err, response, cmd.UsageString())
+
+ if status.State == algod.FastCatchupState {
+ log.Fatal(style.Red.Render("Node is currently catching up."))
+ }
+
+ // Get the latest catchpoint
+ catchpoint, _, err := algod.GetLatestCatchpoint(httpPkg, status.Network)
+ if err != nil && err.Error() == api.InvalidNetworkParamMsg {
+ log.Fatal("This network does not support fast-catchup.")
+ } else {
+ log.Info(style.Green.Render("Latest Catchpoint: " + catchpoint))
+ }
+
+ // Start catchup
+ res, _, err := algod.StartCatchup(ctx, client, catchpoint, nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Info(style.Green.Render(res))
+ },
+}, &dataDir)
diff --git a/cmd/catchup/stop.go b/cmd/catchup/stop.go
new file mode 100644
index 00000000..7b1880d9
--- /dev/null
+++ b/cmd/catchup/stop.go
@@ -0,0 +1,55 @@
+package catchup
+
+import (
+ "context"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+// stopCmdShort provides a concise description of the "stop" command.
+var stopCmdShort = "Stop a fast catchup"
+
+// stopCmdLong provides a detailed description for the "stop" command including its functionality and important notes.
+var stopCmdLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(stopCmdShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Stop an active Fast-Catchup. This will abort the catchup process if one has started",
+ "",
+ style.Yellow.Render("Note: Not all networks support Fast-Catchup."),
+)
+
+// stopCmd is a Cobra command used to check the node's sync status and initiate a fast catchup when necessary.
+var stopCmd = utils.WithAlgodFlags(&cobra.Command{
+ Use: "stop",
+ Short: stopCmdShort,
+ Long: stopCmdLong,
+ SilenceUsage: true,
+ Run: func(cmd *cobra.Command, args []string) {
+ ctx := context.Background()
+ httpPkg := new(api.HttpPkg)
+ client, err := algod.GetClient(dataDir)
+ cobra.CheckErr(err)
+
+ status, response, err := algod.NewStatus(ctx, client, httpPkg)
+ utils.WithInvalidResponsesExplanations(err, response, cmd.UsageString())
+ if status.State != algod.FastCatchupState || status.Catchpoint == nil || *status.Catchpoint == "" {
+ log.Fatal(style.Red.Render("Node is not in fast catchup state."))
+ }
+
+ msg, _, err := algod.AbortCatchup(ctx, client, *status.Catchpoint)
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Info(style.Green.Render("Catchpoint Message: " + msg))
+
+ },
+}, &dataDir)
diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go
new file mode 100644
index 00000000..9cbb6f63
--- /dev/null
+++ b/cmd/configure/configure.go
@@ -0,0 +1,345 @@
+package configure
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "os"
+ "os/exec"
+ "runtime"
+ "strings"
+ "text/template"
+
+ "github.com/spf13/cobra"
+)
+
+// short holds a brief description of the system settings configuration command, currently marked as work in progress.
+var short = "Change settings on the system (WIP)"
+
+// long combines styled strings and descriptions for the detailed command overview displayed in the CLI.
+var long = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(short),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Tool for inspecting and updating the Algorand daemon's config.json and service files",
+ "",
+ style.Yellow.Render(explanations.ExperimentalWarning),
+)
+
+var Cmd = &cobra.Command{
+ Use: "configure",
+ Short: short,
+ Long: long,
+}
+
+func init() {
+ Cmd.AddCommand(serviceCmd)
+}
+
+const RunningErrorMsg = "algorand is currently running. Please stop the node with *node stop* before configuring"
+
+// TODO: configure not just data directory but algod path
+func configureNode() error {
+ var systemServiceConfigure bool
+
+ if algod.IsRunning() {
+ return fmt.Errorf(RunningErrorMsg)
+ }
+
+ // Check systemctl first
+ if algod.IsService() {
+ if promptWrapperYes("Algorand is installed as a service. Do you wish to edit the service file to change the data directory? (y/n)") {
+ // Edit the service file with the user's new data directory
+ systemServiceConfigure = true
+ } else {
+ fmt.Println("Exiting...")
+ os.Exit(0)
+ }
+ }
+
+ // At the end, instead of affectALGORAND_DATA, we'll edit the systemctl algorand.service file
+ // i.e., overwrite /etc/systemd/system/algorand.service.d/override.conf
+ // ExecStart and Description will be changed to reflect the new data directory
+ //
+
+ if !systemServiceConfigure {
+ fmt.Println("Configuring Data directory for algod started through NodeKit...")
+ }
+
+ algorandData := os.Getenv("ALGORAND_DATA")
+
+ // Check if ALGORAND_DATA environment variable is set
+ if algorandData != "" {
+ fmt.Println("ALGORAND_DATA environment variable is set to: " + algorandData)
+ fmt.Println("Inspecting the set data directory...")
+
+ if validateAlgorandDataDir(algorandData) {
+ fmt.Println("Found valid Algorand Data Directory: " + algorandData)
+
+ if systemServiceConfigure {
+ if promptWrapperYes("Would you like to set the ALGORAND_DATA env variable as the data directory for the systemd Algorand service? (y/n)") {
+ editAlgorandServiceFile(algorandData)
+ os.Exit(0)
+ }
+ }
+
+ if promptWrapperNo("Do you want to set a completely new data directory? (y/n)") {
+ fmt.Println("User chose not to set a completely new data directory.")
+ os.Exit(0)
+ }
+
+ if promptWrapperYes("Do you want to manually input the new data directory? (y/n)") {
+ newPath := promptWrapperInput("Enter the new data directory path")
+
+ if !validateAlgorandDataDir(newPath) {
+ fmt.Println("Path at ALGORAND_DATA: " + newPath + " is not recognizable as an Algorand Data directory.")
+ os.Exit(1)
+ }
+
+ if systemServiceConfigure {
+ // Edit the service file
+ editAlgorandServiceFile(newPath)
+ } else {
+ // Affect the ALGORAND_DATA environment variable
+ affectALGORAND_DATA(newPath)
+ }
+ os.Exit(0)
+ }
+ } else {
+ fmt.Println("Path at ALGORAND_DATA: " + algorandData + " is not recognizable as an Algorand Data directory.")
+ }
+ } else {
+ fmt.Println("ALGORAND_DATA environment variable not set.")
+ }
+
+ // Do quick "lazy" check for existing Algorand Data directories
+ paths := utils.GetKnownDataPaths()
+
+ if len(paths) != 0 {
+
+ fmt.Println("Quick check found the following potential data directories:")
+ for _, path := range paths {
+ fmt.Println("โ " + path)
+ }
+
+ if len(paths) == 1 {
+ if promptWrapperYes("Do you want to set this directory as the new data directory? (y/n)") {
+ if systemServiceConfigure {
+ // Edit the service file
+ editAlgorandServiceFile(paths[0])
+ } else {
+ affectALGORAND_DATA(paths[0])
+ }
+ os.Exit(0)
+ }
+
+ } else {
+
+ if promptWrapperYes("Do you want to set one of these directories as the new data directory? (y/n)") {
+
+ selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
+
+ if systemServiceConfigure {
+ // Edit the service file
+ editAlgorandServiceFile(selectedPath)
+ } else {
+ affectALGORAND_DATA(selectedPath)
+ }
+ os.Exit(0)
+ }
+ }
+ }
+
+ // Deep search
+ if promptWrapperNo("Do you want NodeKit to do a deep search for pre-existing Algorand Data directories? (y/n)") {
+ fmt.Println("User chose not to search for more pre-existing Algorand Data directories. Exiting...")
+ os.Exit(0)
+ }
+
+ fmt.Println("Searching for pre-existing Algorand Data directories in HOME directory...")
+ paths = deepSearchAlgorandDataDirs()
+
+ if len(paths) == 0 {
+ fmt.Println("No Algorand data directories could be found in HOME directory. Are you sure Algorand node has been setup? Please run install command.")
+ os.Exit(1)
+ }
+
+ fmt.Println("Found Algorand data directories:")
+ for _, path := range paths {
+ fmt.Println(path)
+ }
+
+ // Prompt user to select a directory
+ selectedPath := promptWrapperSelection("Select an Algorand data directory", paths)
+
+ if systemServiceConfigure {
+ editAlgorandServiceFile(selectedPath)
+ } else {
+ affectALGORAND_DATA(selectedPath)
+ }
+ return nil
+}
+
+func editAlgorandServiceFile(dataDirectoryPath string) {
+ switch runtime.GOOS {
+ case "linux":
+ editSystemdAlgorandServiceFile(dataDirectoryPath)
+ case "darwin":
+ editLaunchdAlgorandServiceFile(dataDirectoryPath)
+ default:
+ fmt.Println("Unsupported operating system.")
+ }
+}
+
+func editLaunchdAlgorandServiceFile(dataDirectoryPath string) {
+
+ algodPath, err := exec.LookPath("algod")
+ if err != nil {
+ fmt.Printf("Failed to find algod binary: %v\n", err)
+ os.Exit(1)
+ }
+
+ overwriteFilePath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+
+ overwriteTemplate := `
+
+
+
+ Label
+ com.algorand.algod
+ ProgramArguments
+
+ {{.AlgodPath}}
+ -d
+ {{.DataDirectoryPath}}
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /tmp/algod.out
+ StandardErrorPath
+ /tmp/algod.err
+
+ `
+
+ // Data to fill the template
+ data := map[string]string{
+ "AlgodPath": algodPath,
+ "DataDirectoryPath": dataDirectoryPath,
+ }
+
+ // Parse and execute the template
+ tmpl, err := template.New("override").Parse(overwriteTemplate)
+ if err != nil {
+ fmt.Printf("Failed to parse template: %v\n", err)
+ os.Exit(1)
+ }
+
+ var overwriteContent bytes.Buffer
+ err = tmpl.Execute(&overwriteContent, data)
+ if err != nil {
+ fmt.Printf("Failed to execute template: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Write the override content to the file
+ err = os.WriteFile(overwriteFilePath, overwriteContent.Bytes(), 0644)
+ if err != nil {
+ fmt.Printf("Failed to write override file: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Boot out the launchd service (just in case - it should be off)
+ cmd := exec.Command("launchctl", "bootout", "system", overwriteFilePath)
+ err = cmd.Run()
+ if err != nil {
+ if !strings.Contains(err.Error(), "No such process") {
+ fmt.Printf("Failed to bootout launchd service: %v\n", err)
+ os.Exit(1)
+ }
+ }
+
+ // Load the launchd service
+ cmd = exec.Command("launchctl", "load", overwriteFilePath)
+ err = cmd.Run()
+ if err != nil {
+ fmt.Printf("Failed to load launchd service: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Println("Launchd service updated and reloaded successfully.")
+}
+
+// Update the algorand.service file
+func editSystemdAlgorandServiceFile(dataDirectoryPath string) {
+
+ algodPath, err := exec.LookPath("algod")
+ if err != nil {
+ fmt.Printf("Failed to find algod binary: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Path to the systemd service override file
+ // Assuming that this is the same everywhere systemd is used
+ overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf"
+
+ // Create the override directory if it doesn't exist
+ err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755)
+ if err != nil {
+ fmt.Printf("Failed to create override directory: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Content of the override file
+ const overrideTemplate = `[Unit]
+Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}}
+[Service]
+ExecStart=
+ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
+
+ // Data to fill the template
+ data := map[string]string{
+ "AlgodPath": algodPath,
+ "DataDirectoryPath": dataDirectoryPath,
+ }
+
+ // Parse and execute the template
+ tmpl, err := template.New("override").Parse(overrideTemplate)
+ if err != nil {
+ fmt.Printf("Failed to parse template: %v\n", err)
+ os.Exit(1)
+ }
+
+ var overrideContent bytes.Buffer
+ err = tmpl.Execute(&overrideContent, data)
+ if err != nil {
+ fmt.Printf("Failed to execute template: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Write the override content to the file
+ err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644)
+ if err != nil {
+ fmt.Printf("Failed to write override file: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Reload systemd manager configuration
+ cmd := exec.Command("systemctl", "daemon-reload")
+ err = cmd.Run()
+ if err != nil {
+ fmt.Printf("Failed to reload systemd daemon: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Println("Algorand service file updated successfully.")
+}
diff --git a/cmd/configure/network.go b/cmd/configure/network.go
new file mode 100644
index 00000000..336e0852
--- /dev/null
+++ b/cmd/configure/network.go
@@ -0,0 +1,16 @@
+package configure
+
+import "github.com/spf13/cobra"
+
+var networkCmd = &cobra.Command{
+ Use: "network",
+ Short: "Configure network",
+ Long: "",
+ Run: func(cmd *cobra.Command, args []string) {
+
+ },
+}
+
+func init() {
+ networkCmd.Flags().StringP("network", "n", "mainnet", "Network to configure")
+}
diff --git a/cmd/configure/service.go b/cmd/configure/service.go
new file mode 100644
index 00000000..ee0af549
--- /dev/null
+++ b/cmd/configure/service.go
@@ -0,0 +1,45 @@
+package configure
+
+import (
+ "errors"
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/spf13/cobra"
+)
+
+// serviceShort provides a brief description of the service command, emphasizing its role in installing service files.
+var serviceShort = "Install service files for the Algorand daemon."
+
+// serviceLong provides a detailed description of the service command, its purpose, and an experimental warning note.
+var serviceLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(serviceShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Ensuring that the Algorand daemon is installed and running as a service.",
+ "",
+ style.Yellow.Render(explanations.ExperimentalWarning),
+)
+
+// serviceCmd is a Cobra command for managing Algorand service files, requiring root privileges to ensure proper execution.
+var serviceCmd = &cobra.Command{
+ Use: "service",
+ Short: serviceShort,
+ Long: serviceLong,
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ if !system.IsSudo() {
+ return errors.New(
+ "you need to be root to run this command. Please run this command with sudo")
+ }
+ return nil
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // TODO: Combine this with algod.UpdateService and algod.SetNetwork
+ return algod.EnsureService()
+ },
+}
diff --git a/cmd/configure/utils.go b/cmd/configure/utils.go
new file mode 100644
index 00000000..a9222d2c
--- /dev/null
+++ b/cmd/configure/utils.go
@@ -0,0 +1,97 @@
+package configure
+
+import (
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/manifoldco/promptui"
+ "github.com/spf13/cobra"
+ "os"
+)
+
+type Release struct {
+ Name string `json:"name"`
+ ZipballURL string `json:"zipball_url"`
+ TarballURL string `json:"tarball_url"`
+ Commit struct {
+ Sha string `json:"sha"`
+ URL string `json:"url"`
+ } `json:"commit"`
+ NodeID string `json:"node_id"`
+}
+
+// Queries user on the provided prompt and returns the user input
+func promptWrapperInput(promptLabel string) string {
+ prompt := promptui.Prompt{
+ Label: promptLabel,
+ }
+
+ result, err := prompt.Run()
+ cobra.CheckErr(err)
+
+ return result
+}
+
+// Queries user on the provided prompt and returns true if user inputs "y"
+func promptWrapperYes(promptLabel string) bool {
+ return promptWrapperInput(promptLabel) == "y"
+}
+
+// Queries user on the provided prompt and returns true if user does not input "y"
+// Included for improved readability of decision tree, despite being redundant.
+func promptWrapperNo(promptLabel string) bool {
+ return promptWrapperInput(promptLabel) != "y"
+}
+
+// Queries user on the provided prompt and returns the selected item
+func promptWrapperSelection(promptLabel string, items []string) string {
+ prompt := promptui.Select{
+ Label: promptLabel,
+ Items: items,
+ }
+
+ _, result, err := prompt.Run()
+ cobra.CheckErr(err)
+
+ fmt.Printf("You selected: %s\n", result)
+
+ return result
+}
+
+// TODO: consider replacing with a method that does more for the user
+func affectALGORAND_DATA(path string) {
+ fmt.Println("Please execute the following in your terminal to set the environment variable:")
+ fmt.Println("")
+ fmt.Println("export ALGORAND_DATA=" + path)
+ fmt.Println("")
+}
+
+func validateAlgorandDataDir(path string) bool {
+ info, err := os.Stat(path)
+
+ // Check if the path exists
+ if os.IsNotExist(err) {
+ return false
+ }
+
+ // Check if the path is a directory
+ if !info.IsDir() {
+ return false
+ }
+
+ paths := system.FindPathToFile(path, "algod.token")
+ if len(paths) == 1 {
+ return true
+ }
+ return false
+}
+
+// Checks if Algorand data directories exist, based off of existence of the "algod.token" file
+func deepSearchAlgorandDataDirs() []string {
+ home, err := os.UserHomeDir()
+ cobra.CheckErr(err)
+
+ // TODO: consider a better way to identify an Algorand data directory
+ paths := system.FindPathToFile(home, "algod.token")
+
+ return paths
+}
diff --git a/cmd/debug.go b/cmd/debug.go
new file mode 100644
index 00000000..1a5c1515
--- /dev/null
+++ b/cmd/debug.go
@@ -0,0 +1,95 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ cmdutils "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+ "os/exec"
+)
+
+// DebugInfo represents diagnostic information about
+// the Algod service, path availability, and related metadata.
+type DebugInfo struct {
+
+ // InPath indicates whether the `algod` command-line tool is available in the system's executable path.
+ InPath bool `json:"inPath"`
+
+ // IsRunning indicates whether the `algod` process is currently running on the host system, returning true if active.
+ IsRunning bool `json:"isRunning"`
+
+ // IsService indicates whether the Algorand software is configured as a system service on the current operating system.
+ IsService bool `json:"isService"`
+
+ // IsInstalled indicates whether the Algorand software is installed on the system by checking its presence and configuration.
+ IsInstalled bool `json:"isInstalled"`
+
+ // Algod holds the path to the `algod` executable if found on the system, or an empty string if not found.
+ Algod string `json:"algod"`
+
+ DataFolder utils.DataFolderConfig `json:"data"`
+}
+
+// debugCmdShort provides a brief description of the "debug" command, which displays debugging information.
+var debugCmdShort = "Display debugging information"
+
+// debugCmdLong provides a detailed description of the "debug" command, outlining its purpose and functionality.
+var debugCmdLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.BANNER,
+ "",
+ style.Bold(debugCmdShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Prints the known state of the nodekit",
+ "Checks various paths and configurations to present useful information for bug reports.",
+ "",
+)
+
+// debugCmd defines the "debug" command used to display diagnostic information for developers, including debug data.
+var debugCmd = cmdutils.WithAlgodFlags(&cobra.Command{
+ Use: "debug",
+ Short: debugCmdShort,
+ Long: debugCmdLong,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ log.Info("Collecting debug information...")
+
+ // Warn user for prompt
+ log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
+
+ path, _ := exec.LookPath("algod")
+
+ dataDir, err := algod.GetDataDir("")
+ if err != nil {
+ return err
+ }
+ folderDebug, err := utils.ToDataFolderConfig(dataDir)
+ if err != nil {
+ return err
+ }
+ info := DebugInfo{
+ InPath: system.CmdExists("algod"),
+ IsRunning: algod.IsRunning(),
+ IsService: algod.IsService(),
+ IsInstalled: algod.IsInstalled(),
+ Algod: path,
+ DataFolder: folderDebug,
+ }
+ data, err := json.MarshalIndent(info, "", " ")
+ if err != nil {
+ return err
+ }
+
+ log.Info(style.Blue.Render("Copy and paste the following to a bug report:"))
+ fmt.Println(style.Bold(string(data)))
+ return nil
+ },
+}, &algodData)
diff --git a/cmd/install.go b/cmd/install.go
new file mode 100644
index 00000000..5f25fa17
--- /dev/null
+++ b/cmd/install.go
@@ -0,0 +1,77 @@
+package cmd
+
+import (
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+ "os"
+ "time"
+)
+
+// InstallMsg is a constant string used to indicate the start of the Algorand installation process with a specific message.
+const InstallMsg = "Installing Algorand"
+
+// InstallExistsMsg is a constant string used to indicate that the Algod is already installed on the system.
+const InstallExistsMsg = "algod is already installed"
+
+var installShort = "Install the node daemon"
+
+var installLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(installShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Configures the local package manager and installs the algorand daemon on your local machine",
+ "",
+)
+
+// installCmd is a Cobra command that installs the Algorand daemon on the local machine, ensuring the service is operational.
+var installCmd = &cobra.Command{
+ Use: "install",
+ Short: installShort,
+ Long: installLong,
+ SilenceUsage: true,
+ Run: func(cmd *cobra.Command, args []string) {
+ // TODO: yes flag
+
+ // TODO: get expected version
+ log.Info(style.Green.Render(InstallMsg))
+ // Warn user for prompt
+ log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
+
+ // TODO: compare expected version to existing version
+ if algod.IsInstalled() && !force {
+ log.Error(InstallExistsMsg)
+ os.Exit(1)
+ }
+
+ // Run the installation
+ err := algod.Install()
+ if err != nil {
+ log.Error(err)
+ os.Exit(1)
+ }
+
+ time.Sleep(5 * time.Second)
+
+ // If it's not running, start the daemon (can happen)
+ if !algod.IsRunning() {
+ err = algod.Start()
+ if err != nil {
+ log.Error(err)
+ os.Exit(1)
+ }
+ }
+
+ log.Info(style.Green.Render("Algorand installed successfully ๐"))
+ },
+}
+
+func init() {
+ installCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully install the node"))
+}
diff --git a/cmd/root.go b/cmd/root.go
index 3e84283d..65ccdba6 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,117 +2,84 @@ package cmd
import (
"context"
- "encoding/json"
- "fmt"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui"
- "github.com/algorandfoundation/algorun-tui/ui/explanations"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/cmd/catchup"
+ "github.com/algorandfoundation/nodekit/cmd/configure"
+ "github.com/algorandfoundation/nodekit/cmd/utils"
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/algorandfoundation/nodekit/ui"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
- "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
"github.com/spf13/cobra"
- "github.com/spf13/viper"
- "io"
- "os"
- "strings"
+ "runtime"
)
-const BANNER = `
- _____ .__ __________
- / _ \ | | ____ ____\______ \__ __ ____
- / /_\ \| | / ___\ / _ \| _/ | \/ \
-/ | \ |__/ /_/ > <_> ) | \ | / | \
-\____|__ /____/\___ / \____/|____|_ /____/|___| /
- \/ /_____/ \/ \/
-`
-
var (
- algod string
- token = strings.Repeat("a", 64)
+ Name = "nodekit"
+
+ // algodEndpoint defines the URI address of the Algorand node, including the protocol (http/https), for client communication.
+ algodData string
+
+ // Version represents the application version string, which is set during build or defaults to "unknown".
Version = ""
- rootCmd = &cobra.Command{
- Use: "algorun",
- Short: "Manage Algorand nodes",
- Long: style.Purple(BANNER) + "\n",
+
+ // force indicates whether actions should be performed forcefully, bypassing checks or confirmations.
+ force bool = false
+
+ short = "Manage Algorand nodes from the command line"
+ long = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(short),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Welcome to NodeKit, a TUI for managing Algorand nodes.",
+ "A one stop shop for managing Algorand nodes, including node creation, configuration, and management.",
+ "",
+ style.Yellow.Render(explanations.ExperimentalWarning),
+ )
+ // RootCmd is the primary command for managing Algorand nodes, providing CLI functionality and TUI for interaction.
+ RootCmd = utils.WithAlgodFlags(&cobra.Command{
+ Use: Name,
+ Version: Version,
+ Short: short,
+ Long: long,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
- RunE: func(cmd *cobra.Command, args []string) error {
+ Run: func(cmd *cobra.Command, args []string) {
log.SetOutput(cmd.OutOrStdout())
- initConfig()
-
- if viper.GetString("algod-endpoint") == "" {
- return fmt.Errorf(style.Red.Render("algod-endpoint is required") + explanations.NodeNotFound)
- }
-
- if viper.GetString("algod-token") == "" {
- return fmt.Errorf(style.Red.Render("algod-token is required"))
- }
-
- client, err := getClient()
- cobra.CheckErr(err)
-
+ // Create the dependencies
ctx := context.Background()
- v, err := client.GetStatusWithResponse(ctx)
- if err != nil {
- return fmt.Errorf(
- style.Red.Render("failed to get status: %s")+explanations.Unreachable,
- err)
- } else if v.StatusCode() == 401 {
- return fmt.Errorf(
- style.Red.Render("failed to get status: Unauthorized") + explanations.TokenInvalid)
- } else if v.StatusCode() != 200 {
- return fmt.Errorf(
- style.Red.Render("failed to get status: error code %d")+explanations.TokenNotAdmin,
- v.StatusCode())
- }
-
- partkeys, err := internal.GetPartKeys(ctx, client)
- if err != nil {
- return fmt.Errorf(
- style.Red.Render("failed to get participation keys: %s")+
- explanations.TokenNotAdmin,
- err)
- }
- state := internal.StateModel{
- Status: internal.StatusModel{
- State: "INITIALIZING",
- Version: "N/A",
- Network: "N/A",
- Voting: false,
- NeedsUpdate: true,
- LastRound: 0,
- },
- Metrics: internal.MetricsModel{
- RoundTime: 0,
- TPS: 0,
- RX: 0,
- TX: 0,
- },
- ParticipationKeys: partkeys,
-
- Client: client,
- Context: ctx,
- }
- state.Accounts, err = internal.AccountsFromState(&state, new(internal.Clock), client)
+ client, err := algod.GetClient(algodData)
cobra.CheckErr(err)
- // Fetch current state
- err = state.Status.Fetch(ctx, client, new(internal.HttpPkg))
+ httpPkg := new(api.HttpPkg)
+ t := new(system.Clock)
+ // Fetch the state and handle any creation errors
+ state, stateResponse, err := algod.NewStateModel(ctx, client, httpPkg)
+ utils.WithInvalidResponsesExplanations(err, stateResponse, cmd.UsageString())
cobra.CheckErr(err)
- m, err := ui.NewViewportViewModel(&state, client)
+ // Construct the TUI Model from the State
+ m, err := ui.NewViewportViewModel(state, client)
cobra.CheckErr(err)
+ // Construct the TUI Application
p := tea.NewProgram(
m,
tea.WithAltScreen(),
tea.WithFPS(120),
)
+
+ // Watch for State Updates on a separate thread
+ // TODO: refactor into context aware watcher without callbacks
go func() {
- state.Watch(func(status *internal.StateModel, err error) {
+ state.Watch(func(status *algod.StateModel, err error) {
if err == nil {
p.Send(state)
}
@@ -120,165 +87,62 @@ var (
p.Send(state)
p.Send(err)
}
- }, ctx, client)
+ }, ctx, t)
}()
+
+ // Execute the TUI Application
_, err = p.Run()
- return err
+ if err != nil {
+ log.Fatal(err)
+ }
},
- }
+ }, &algodData)
)
-func check(err interface{}) {
- if err != nil {
- panic(err)
+// NeedsToBeRunning ensures the Algod software is installed and running before executing the associated Cobra command.
+func NeedsToBeRunning(cmd *cobra.Command, args []string) {
+ if force {
+ return
+ }
+ if !algod.IsInstalled() {
+ log.Fatal(explanations.NotInstalledErrorMsg)
+ }
+ if !algod.IsRunning() {
+ log.Fatal(explanations.NotRunningErrorMsg)
}
}
-// Handle global flags and set usage templates
-func init() {
- log.SetReportTimestamp(false)
-
- // Configure Version
- if Version == "" {
- Version = "unknown (built from source)"
+// NeedsToBeStopped ensures the operation halts if Algod is not installed or is currently running, unless forced.
+func NeedsToBeStopped(cmd *cobra.Command, args []string) {
+ if force {
+ return
}
- rootCmd.Version = Version
-
- // Bindings
- rootCmd.PersistentFlags().StringVarP(&algod, "algod-endpoint", "a", "", style.LightBlue("algod endpoint address URI, including http[s]"))
- rootCmd.PersistentFlags().StringVarP(&token, "algod-token", "t", "", lipgloss.JoinHorizontal(
- lipgloss.Left,
- style.LightBlue("algod "),
- style.BoldUnderline("admin"),
- style.LightBlue(" token"),
- ))
- _ = viper.BindPFlag("algod-endpoint", rootCmd.PersistentFlags().Lookup("algod-endpoint"))
- _ = viper.BindPFlag("algod-token", rootCmd.PersistentFlags().Lookup("algod-token"))
-
- // Update Long Text
- rootCmd.Long +=
- style.Magenta("Configuration: ") + viper.GetViper().ConfigFileUsed() + "\n" +
- style.LightBlue("Algod: ") + viper.GetString("algod-endpoint")
-
- if viper.GetString("data") != "" {
- rootCmd.Long +=
- style.Magenta("\nAlgorand Data: ") + viper.GetString("data")
+ if !algod.IsInstalled() {
+ log.Fatal(explanations.NotInstalledErrorMsg)
+ }
+ if algod.IsRunning() {
+ log.Fatal(explanations.RunningErrorMsg)
}
+}
+// init initializes the application, setting up logging, commands, and version information.
+func init() {
+ log.SetReportTimestamp(false)
// Add Commands
- rootCmd.AddCommand(statusCmd)
+ if runtime.GOOS != "windows" {
+ RootCmd.AddCommand(bootstrapCmd)
+ RootCmd.AddCommand(debugCmd)
+ RootCmd.AddCommand(installCmd)
+ RootCmd.AddCommand(startCmd)
+ RootCmd.AddCommand(stopCmd)
+ RootCmd.AddCommand(uninstallCmd)
+ RootCmd.AddCommand(upgradeCmd)
+ RootCmd.AddCommand(catchup.Cmd)
+ RootCmd.AddCommand(configure.Cmd)
+ }
}
// Execute executes the root command.
func Execute() error {
- return rootCmd.Execute()
-}
-
-type AlgodConfig struct {
- EndpointAddress string `json:"EndpointAddress"`
-}
-
-func replaceEndpointUrl(s string) string {
- s = strings.Replace(s, "\n", "", 1)
- s = strings.Replace(s, "0.0.0.0", "127.0.0.1", 1)
- s = strings.Replace(s, "[::]", "127.0.0.1", 1)
- return s
-}
-func hasWildcardEndpointUrl(s string) bool {
- return strings.Contains(s, "0.0.0.0") || strings.Contains(s, "::")
-}
-func initConfig() {
- // Find home directory.
- home, err := os.UserHomeDir()
- cobra.CheckErr(err)
-
- // Look for paths
- viper.AddConfigPath(".")
- viper.AddConfigPath(home)
- viper.AddConfigPath("/etc/algorun/")
-
- // Set Config Properties
- viper.SetConfigType("yaml")
- viper.SetConfigName(".algorun")
- viper.SetEnvPrefix("algorun")
-
- // Load Configurations
- viper.AutomaticEnv()
- _ = viper.ReadInConfig()
-
- // Check for algod
- loadedAlgod := viper.GetString("algod-endpoint")
- loadedToken := viper.GetString("algod-token")
-
- // Load ALGORAND_DATA/config.json
- algorandData, exists := os.LookupEnv("ALGORAND_DATA")
-
- // Load the Algorand Data Configuration
- if exists && algorandData != "" && loadedAlgod == "" {
- // Placeholder for Struct
- var algodConfig AlgodConfig
-
- dataConfigPath := algorandData + "/config.json"
-
- // Open the config.json File
- configFile, err := os.Open(dataConfigPath)
- check(err)
-
- // Read the bytes of the File
- byteValue, _ := io.ReadAll(configFile)
- err = json.Unmarshal(byteValue, &algodConfig)
- check(err)
-
- // Close the open handle
- err = configFile.Close()
- check(err)
-
- // Check for endpoint address
- if hasWildcardEndpointUrl(algodConfig.EndpointAddress) {
- algodConfig.EndpointAddress = replaceEndpointUrl(algodConfig.EndpointAddress)
- } else if algodConfig.EndpointAddress == "" {
- // Assume it is not set, try to discover the port from the network file
- networkPath := algorandData + "/algod.net"
- networkFile, err := os.Open(networkPath)
- check(err)
-
- byteValue, err = io.ReadAll(networkFile)
- check(err)
-
- if hasWildcardEndpointUrl(string(byteValue)) {
- algodConfig.EndpointAddress = replaceEndpointUrl(string(byteValue))
- } else {
- algodConfig.EndpointAddress = string(byteValue)
- }
-
- }
- if strings.Contains(algodConfig.EndpointAddress, ":0") {
- algodConfig.EndpointAddress = strings.Replace(algodConfig.EndpointAddress, ":0", ":8080", 1)
- }
- if loadedToken == "" {
- // Handle Token Path
- tokenPath := algorandData + "/algod.admin.token"
-
- tokenFile, err := os.Open(tokenPath)
- check(err)
-
- byteValue, err = io.ReadAll(tokenFile)
- check(err)
-
- viper.Set("algod-token", strings.Replace(string(byteValue), "\n", "", 1))
- }
-
- // Set the algod configuration
- viper.Set("algod-endpoint", "http://"+strings.Replace(algodConfig.EndpointAddress, "\n", "", 1))
- viper.Set("data", dataConfigPath)
- }
-
-}
-
-func getClient() (*api.ClientWithResponses, error) {
- apiToken, err := securityprovider.NewSecurityProviderApiKey("header", "X-Algo-API-Token", viper.GetString("algod-token"))
- if err != nil {
- return nil, err
- }
- return api.NewClientWithResponses(viper.GetString("algod-endpoint"), api.WithRequestEditorFn(apiToken.Intercept))
+ return RootCmd.Execute()
}
diff --git a/cmd/root_test.go b/cmd/root_test.go
deleted file mode 100644
index 3ce475bf..00000000
--- a/cmd/root_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package cmd
-
-import (
- "errors"
- "github.com/spf13/viper"
- "os"
- "testing"
-)
-
-func clearViper() {
- viper.Set("algod-token", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
- viper.Set("algod-endpoint", "http://localhost:8080")
- viper.Set("ALGORAND_DATA", "")
-}
-
-// Test the stub root command
-func Test_ExecuteRootCommand(t *testing.T) {
- clearViper()
-
- // Execute
- err := rootCmd.Execute()
- // Should always fail due to no TTY
- if err == nil {
- t.Fatal(err)
- }
-
- t.Run("Invalid algod-endpoint", func(t *testing.T) {
- viper.Set("algod-endpoint", "")
- err := rootCmd.Execute()
- if err == nil {
- t.Error("No error for invalid algod-endpoint")
- }
- clearViper()
- })
- t.Run("Invalid algod-token", func(t *testing.T) {
- viper.Set("algod-endpoint", "http://localhost:8080")
- viper.Set("algod-token", "")
- err := rootCmd.Execute()
- if err == nil {
- t.Error("No error for invalid algod-endpoint")
- }
- })
- t.Run("InitConfig", func(t *testing.T) {
- cwd, _ := os.Getwd()
- viper.Set("algod-token", "")
- viper.Set("algod-endpoint", "")
- t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfig")
-
- initConfig()
- algod := viper.Get("algod-endpoint")
- if algod == "" {
- t.Fatal("Invalid Algod")
- }
- if algod != "http://127.0.0.1:8080" {
- t.Fatal("Invalid Algod")
- }
- clearViper()
- })
-
- t.Run("InitConfigWithoutEndpoint", func(t *testing.T) {
- cwd, _ := os.Getwd()
- viper.Set("algod-token", "")
- viper.Set("algod-endpoint", "")
- t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithoutEndpoint")
-
- initConfig()
- algod := viper.Get("algod-endpoint")
- if algod == "" {
- t.Fatal("Invalid Algod")
- }
- if algod != "http://127.0.0.1:8080" {
- t.Fatal("Invalid Algod")
- }
- clearViper()
- })
-
- t.Run("InitConfigWithAddress", func(t *testing.T) {
- cwd, _ := os.Getwd()
- viper.Set("algod-token", "")
- viper.Set("algod-endpoint", "")
- t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithAddress")
-
- initConfig()
- algod := viper.Get("algod-endpoint")
- if algod == "" {
- t.Fatal("Invalid Algod")
- }
- if algod != "http://255.255.255.255:8080" {
- t.Fatal("Invalid Algod")
- }
- clearViper()
- })
-
- t.Run("InitConfigWithAddressAndDefaultPort", func(t *testing.T) {
- cwd, _ := os.Getwd()
- viper.Set("algod-token", "")
- viper.Set("algod-endpoint", "")
- t.Setenv("ALGORAND_DATA", cwd+"/testdata/Test_InitConfigWithAddressAndDefaultPort")
-
- initConfig()
- algod := viper.Get("algod-endpoint")
- if algod == "" {
- t.Fatal("Invalid Algod")
- }
- if algod != "http://255.255.255.255:8080" {
- t.Fatal("Invalid Algod")
- }
- clearViper()
- })
-
- t.Run("check error", func(t *testing.T) {
- defer func() {
- if r := recover(); r == nil {
- t.Errorf("The code did not panic")
- }
- }()
- check(errors.New("test"))
- })
-}
diff --git a/cmd/start.go b/cmd/start.go
new file mode 100644
index 00000000..c95400b8
--- /dev/null
+++ b/cmd/start.go
@@ -0,0 +1,48 @@
+package cmd
+
+import (
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+var startShort = "Start the node daemon"
+
+var startLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(startShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Start the Algorand daemon on your local machine if it is not already running. Optionally, the daemon can be forcefully started.",
+ "",
+ style.Yellow.Render("This requires the daemon to be installed on your system."),
+)
+
+// startCmd is a Cobra command used to start the Algod service on the system, ensuring necessary checks are performed beforehand.
+var startCmd = &cobra.Command{
+ Use: "start",
+ Short: startShort,
+ Long: startLong,
+ SilenceUsage: true,
+ PersistentPreRun: NeedsToBeStopped,
+ Run: func(cmd *cobra.Command, args []string) {
+ log.Info(style.Green.Render("Starting Algod ๐"))
+ // Warn user for prompt
+ log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
+ err := algod.Start()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Info(style.Green.Render("Algorand started successfully ๐"))
+ },
+}
+
+// init initializes the `force` flag for the `start` command, allowing the node to start forcefully when specified.
+func init() {
+ startCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully start the node"))
+}
diff --git a/cmd/status.go b/cmd/status.go
deleted file mode 100644
index 5ab65081..00000000
--- a/cmd/status.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package cmd
-
-import (
- "context"
- "errors"
- "fmt"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui"
- "github.com/algorandfoundation/algorun-tui/ui/style"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
- "os"
-)
-
-// statusCmd is the main entrypoint for the `status` cobra.Command with a tea.Program
-var statusCmd = &cobra.Command{
- Use: "status",
- Short: "Get the node status",
- Long: style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"),
- RunE: func(cmd *cobra.Command, args []string) error {
- initConfig()
- if viper.GetString("algod-endpoint") == "" {
- return errors.New(style.Magenta("algod-endpoint is required"))
- }
-
- // Get Algod from configuration
- client, err := getClient()
- cobra.CheckErr(err)
- state := internal.StateModel{
- Status: internal.StatusModel{
- State: "SYNCING",
- Version: "N/A",
- Network: "N/A",
- Voting: false,
- NeedsUpdate: true,
- LastRound: 0,
- },
- Metrics: internal.MetricsModel{
- RoundTime: 0,
- TPS: 0,
- RX: 0,
- TX: 0,
- },
- ParticipationKeys: nil,
- }
- err = state.Status.Fetch(context.Background(), client, new(internal.HttpPkg))
- cobra.CheckErr(err)
- // Create the TUI
- view := ui.MakeStatusViewModel(&state)
-
- p := tea.NewProgram(view, tea.WithAltScreen())
- go func() {
- state.Watch(func(status *internal.StateModel, err error) {
- cobra.CheckErr(err)
- p.Send(state)
- }, context.Background(), client)
- }()
- // Execute the Command
- if _, err := p.Run(); err != nil {
- fmt.Printf("Alas, there's been an error: %v", err)
- os.Exit(1)
- }
- return nil
- },
-}
diff --git a/cmd/status_test.go b/cmd/status_test.go
deleted file mode 100644
index d6ff576d..00000000
--- a/cmd/status_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package cmd
-
-import (
- "context"
- "github.com/spf13/viper"
- "testing"
-)
-
-func Test_ExecuteInvalidStatusCommand(t *testing.T) {
- viper.Set("algod-endpoint", "")
- err := statusCmd.RunE(nil, nil)
- if err == nil {
- t.Error("Must fail when algod-endpoint is missing")
- }
-}
-
-// Test the Status Command
-func Test_ExecuteStatusCommand(t *testing.T) {
- // Smoke Test Command
- viper.Set("algod-endpoint", "https://mainnet-api.4160.nodely.dev:443")
- go func() {
- err := statusCmd.RunE(nil, []string{"--algod-endpoint", "https://mainnet-api.4160.nodely.dev:443"})
- if err != nil {
-
- }
- }()
- context.Background().Done()
-}
diff --git a/cmd/stop.go b/cmd/stop.go
new file mode 100644
index 00000000..553d4d20
--- /dev/null
+++ b/cmd/stop.go
@@ -0,0 +1,65 @@
+package cmd
+
+import (
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "time"
+
+ "github.com/spf13/cobra"
+)
+
+// StopTimeout defines the duration to wait after attempting to stop the Algod process to ensure it has fully shut down.
+const StopTimeout = 5 * time.Second
+
+const StoppingAlgodMsg = "Stopping Algod ๐ข"
+
+// StopSuccessMsg is a constant string message indicating that Algod has been stopped successfully.
+const StopSuccessMsg = "Algorand stopped successfully ๐"
+
+// StopFailureMsg is a constant string used as an error message when the Algod process fails to stop.
+const StopFailureMsg = "failed to stop Algod"
+
+var stopShort = "Stop the node daemon"
+
+var stopLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(stopShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Stops the Algorand daemon on your local machine. Optionally, the daemon can be forcefully stopped.",
+ "",
+ style.Yellow.Render("This requires the daemon to be installed on your system."),
+)
+var stopCmd = &cobra.Command{
+ Use: "stop",
+ Short: stopShort,
+ Long: stopLong,
+ SilenceUsage: true,
+ PersistentPreRun: NeedsToBeRunning,
+ Run: func(cmd *cobra.Command, args []string) {
+ log.Info(style.Green.Render(StoppingAlgodMsg))
+ // Warn user for prompt
+ log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
+
+ err := algod.Stop()
+ if err != nil {
+ log.Fatal(StopFailureMsg)
+ }
+ time.Sleep(StopTimeout)
+
+ if algod.IsRunning() {
+ log.Fatal(StopFailureMsg)
+ }
+
+ log.Info(style.Green.Render(StopSuccessMsg))
+ },
+}
+
+func init() {
+ stopCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully stop the node"))
+}
diff --git a/cmd/uninstall.go b/cmd/uninstall.go
new file mode 100644
index 00000000..c0c31553
--- /dev/null
+++ b/cmd/uninstall.go
@@ -0,0 +1,52 @@
+package cmd
+
+import (
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+// UninstallWarningMsg provides a warning message to inform users they may be prompted for their password during uninstallation.
+const UninstallWarningMsg = "(You may be prompted for your password to uninstall)"
+
+var uninstallShort = "Uninstall the node daemon"
+
+var uninstallLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(uninstallShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.",
+ "",
+ style.Yellow.Render("This requires the daemon to be installed on your system."),
+)
+
+// uninstallCmd defines a Cobra command used to uninstall the Algorand node (Algod) and related binaries from the system.
+var uninstallCmd = &cobra.Command{
+ Use: "uninstall",
+ Short: uninstallShort,
+ Long: uninstallLong,
+ SilenceUsage: true,
+ PersistentPreRun: NeedsToBeStopped,
+ Run: func(cmd *cobra.Command, args []string) {
+ if force {
+ log.Warn(style.Red.Render("Uninstalling Algorand (forcefully)"))
+ }
+ // Warn user for prompt
+ log.Warn(style.Yellow.Render(UninstallWarningMsg))
+
+ err := algod.Uninstall(force)
+ if err != nil {
+ log.Fatal(err)
+ }
+ },
+}
+
+// init initializes the uninstall command's flags, including the "force" flag for forced uninstallation.
+func init() {
+ uninstallCmd.Flags().BoolVarP(&force, "force", "f", false, style.Yellow.Render("forcefully uninstall the node"))
+}
diff --git a/cmd/upgrade.go b/cmd/upgrade.go
new file mode 100644
index 00000000..9c7ceb30
--- /dev/null
+++ b/cmd/upgrade.go
@@ -0,0 +1,60 @@
+package cmd
+
+import (
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+ "os"
+ "time"
+)
+
+// UpgradeMsg is a constant string used to indicate the start of the Algod upgrade process.
+const UpgradeMsg = "Upgrading Algod"
+
+var upgradeShort = "Upgrade the node daemon"
+
+var upgradeLong = lipgloss.JoinVertical(
+ lipgloss.Left,
+ style.Purple(style.BANNER),
+ "",
+ style.Bold(upgradeShort),
+ "",
+ style.BoldUnderline("Overview:"),
+ "Upgrade Algorand packages if it was installed with package manager.",
+ "",
+ style.Yellow.Render("This requires the daemon to be installed on your system."),
+)
+
+// upgradeCmd is a Cobra command used to upgrade Algod, utilizing the OS-specific package manager if applicable.
+var upgradeCmd = &cobra.Command{
+ Use: "upgrade",
+ Short: upgradeShort,
+ Long: upgradeLong,
+ SilenceUsage: true,
+ PersistentPreRun: NeedsToBeStopped,
+ Run: func(cmd *cobra.Command, args []string) {
+ // TODO: get expected version and check if update is required
+ log.Info(style.Green.Render(UpgradeMsg))
+ // Warn user for prompt
+ log.Warn(style.Yellow.Render(explanations.SudoWarningMsg))
+ // TODO: Check Version from S3 against the local binary
+ err := algod.Update()
+ if err != nil {
+ log.Error(err)
+ }
+
+ time.Sleep(5 * time.Second)
+
+ // If it's not running, start the daemon (can happen)
+ if !algod.IsRunning() {
+ err = algod.Start()
+ if err != nil {
+ log.Error(err)
+ os.Exit(1)
+ }
+ }
+ },
+}
diff --git a/cmd/utils/config.go b/cmd/utils/config.go
new file mode 100644
index 00000000..dea6f0db
--- /dev/null
+++ b/cmd/utils/config.go
@@ -0,0 +1,14 @@
+package utils
+
+// Config represents the config.json file
+type Config struct {
+ EndpointAddress string `json:"EndpointAddress"`
+}
+
+// DaemonConfig represents the configuration settings for a daemon,
+// including paths, network, token, and sub-configurations.
+type DaemonConfig struct {
+ DataDirectoryPath string `json:"data"`
+ EndpointAddress string `json:"endpoint"`
+ Token string `json:"token"`
+}
diff --git a/cmd/utils/explanations/errors.go b/cmd/utils/explanations/errors.go
new file mode 100644
index 00000000..97b1d47f
--- /dev/null
+++ b/cmd/utils/explanations/errors.go
@@ -0,0 +1,16 @@
+package explanations
+
+// SudoWarningMsg is a constant string displayed to warn users that they may be prompted for their password during execution.
+const SudoWarningMsg = "(You may be prompted for your password)"
+
+// PermissionErrorMsg is a constant string that indicates a command requires super-user privileges (sudo) to be executed.
+const PermissionErrorMsg = "this command must be run with super-user privileges (sudo)"
+
+// NotInstalledErrorMsg is the error message displayed when the algod software is not installed on the system.
+const NotInstalledErrorMsg = "algod is not installed. please run the *install* command"
+
+// RunningErrorMsg represents the error message displayed when algod is running and needs to be stopped before proceeding.
+const RunningErrorMsg = "algod is running, please run the *stop* command"
+
+// NotRunningErrorMsg is the error message displayed when the algod service is not currently running on the system.
+const NotRunningErrorMsg = "algod is not running"
diff --git a/cmd/utils/explanations/explanations.go b/cmd/utils/explanations/explanations.go
new file mode 100644
index 00000000..12672c9e
--- /dev/null
+++ b/cmd/utils/explanations/explanations.go
@@ -0,0 +1,30 @@
+package explanations
+
+import (
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// NodeNotFound is a styled message explaining that the node could not be automatically found and describes how to configure it.
+var NodeNotFound = lipgloss.JoinHorizontal(lipgloss.Left,
+ style.Cyan.Render("Explanation"),
+ style.Bold(": "),
+) +
+ "nodekit could not find your node automatically. Ensure the node is installed and running: If you have not installed algod yet, run \"nodekit bootstrap\". If your node is installed, start it with \"nodekit start\". \n\n" +
+ lipgloss.JoinHorizontal(lipgloss.Left,
+ "Otherwise for custom installations, provide ",
+ style.Bold("--datadir"),
+ " or set the goal-compatible ",
+ style.Bold("ALGORAND_DATA"),
+ " environment variable to the algod data directory, ",
+ style.Bold("e.g. /var/lib/algorand"),
+ ) + "\n"
+
+// Unreachable is an error message indicating inability to connect to algod, suggesting to verify algod is running and configured.
+var Unreachable = "Explanation: Could not reach algod. Check that algod is running and the provided connection arguments.\n"
+
+// TokenInvalid provides an error message indicating the administrative token for algod is invalid or missing.
+var TokenInvalid = "Explanation: algod token is invalid. NodeKit requires the " + style.BoldUnderline("admin token") + " for algod. You can find this in the algod.admin.token file in the algod data directory.\n"
+
+// TokenNotAdmin is an explanatory message shown when the provided token lacks admin privileges for the algod node.
+var TokenNotAdmin = "Explanation: nodekit requires the " + style.BoldUnderline("admin token") + " for algod. You can find this in the algod.admin.token file in the algod data directory.\n"
diff --git a/cmd/utils/explanations/warning.go b/cmd/utils/explanations/warning.go
new file mode 100644
index 00000000..1b2d7e0e
--- /dev/null
+++ b/cmd/utils/explanations/warning.go
@@ -0,0 +1,3 @@
+package explanations
+
+const ExperimentalWarning = "Note: This is still a work in progress. Expect bugs and rough edges."
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
new file mode 100644
index 00000000..e41e6987
--- /dev/null
+++ b/cmd/utils/flags.go
@@ -0,0 +1,40 @@
+package utils
+
+import (
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/cmd/utils/explanations"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+func WithInvalidResponsesExplanations(err error, response api.ResponseInterface, postFix string) {
+ if err != nil && err.Error() == algod.InvalidVersionResponseError {
+ log.Fatal(style.Red.Render("node not found") + "\n\n" + explanations.NodeNotFound + "\n" + postFix)
+ }
+ if response.StatusCode() == 401 {
+ log.Fatal(
+ style.Red.Render("failed to get status: Unauthorized") + "\n\n" + explanations.TokenInvalid + "\n" + postFix)
+ }
+ if response.StatusCode() > 300 {
+ log.Fatal(
+ style.Red.Render("failed to get status: error code %d")+"\n\n"+explanations.TokenNotAdmin+"\n"+postFix,
+ response.StatusCode())
+ }
+}
+
+// WithAlgodFlags enhances a cobra.Command with flags for Algod endpoint and token configuration.
+func WithAlgodFlags(cmd *cobra.Command, algodData *string) *cobra.Command {
+ cmd.Flags().StringVarP(algodData, "datadir", "d", "", style.LightBlue("Data directory for the node"))
+
+ _ = viper.BindPFlag("datadir", cmd.Flags().Lookup("datadir"))
+
+ if viper.GetString("datadir") != "" {
+ cmd.Long +=
+ style.LightBlue(" Data: ") + viper.GetString("datadir") + "\n"
+ }
+
+ return cmd
+}
diff --git a/cmd/testdata/Test_InitConfig/algod.admin.token b/cmd/utils/testdata/Test_InitConfig/algod.admin.token
similarity index 100%
rename from cmd/testdata/Test_InitConfig/algod.admin.token
rename to cmd/utils/testdata/Test_InitConfig/algod.admin.token
diff --git a/cmd/testdata/Test_InitConfig/config.json b/cmd/utils/testdata/Test_InitConfig/config.json
similarity index 100%
rename from cmd/testdata/Test_InitConfig/config.json
rename to cmd/utils/testdata/Test_InitConfig/config.json
diff --git a/cmd/testdata/Test_InitConfigWithAddress/algod.admin.token b/cmd/utils/testdata/Test_InitConfigWithAddress/algod.admin.token
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithAddress/algod.admin.token
rename to cmd/utils/testdata/Test_InitConfigWithAddress/algod.admin.token
diff --git a/cmd/testdata/Test_InitConfigWithAddress/config.json b/cmd/utils/testdata/Test_InitConfigWithAddress/config.json
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithAddress/config.json
rename to cmd/utils/testdata/Test_InitConfigWithAddress/config.json
diff --git a/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token b/cmd/utils/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token
rename to cmd/utils/testdata/Test_InitConfigWithAddressAndDefaultPort/algod.admin.token
diff --git a/cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json b/cmd/utils/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json
rename to cmd/utils/testdata/Test_InitConfigWithAddressAndDefaultPort/config.json
diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token b/cmd/utils/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token
rename to cmd/utils/testdata/Test_InitConfigWithoutEndpoint/algod.admin.token
diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net b/cmd/utils/testdata/Test_InitConfigWithoutEndpoint/algod.net
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithoutEndpoint/algod.net
rename to cmd/utils/testdata/Test_InitConfigWithoutEndpoint/algod.net
diff --git a/cmd/testdata/Test_InitConfigWithoutEndpoint/config.json b/cmd/utils/testdata/Test_InitConfigWithoutEndpoint/config.json
similarity index 100%
rename from cmd/testdata/Test_InitConfigWithoutEndpoint/config.json
rename to cmd/utils/testdata/Test_InitConfigWithoutEndpoint/config.json
diff --git a/codecov.yaml b/codecov.yaml
index 2c7de746..a54b42fa 100644
--- a/codecov.yaml
+++ b/codecov.yaml
@@ -2,8 +2,8 @@ coverage:
status:
project:
default:
- target: 60%
+ target: 40%
threshold: 10%
patch:
default:
- target: 60%
\ No newline at end of file
+ target: 10%
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
deleted file mode 100644
index baf59c83..00000000
--- a/docker-compose.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-services:
- dev:
- build:
- context: .
- dockerfile: Dockerfile
- environment:
- - ADMIN_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- - TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
- tmpfs:
- - /algod/empty
- - /algod/fast-catchup
- volumes:
- - algod:/algod/data
- ports:
- - "8080:8080"
- - "8081:8081"
- - "8082:8082"
-volumes:
- algod:
\ No newline at end of file
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000..6240da8b
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,21 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..bc54e6da
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,55 @@
+# Starlight Starter Kit: Tailwind
+
+[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
+
+```
+npm create astro@latest -- --template starlight/tailwind
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/tailwind)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/tailwind)
+[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/tailwind)
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Ftailwind&project-name=my-starlight-docs&repository-name=my-starlight-docs)
+
+> ๐งโ๐ **Seasoned astronaut?** Delete this file. Have fun!
+
+## ๐ Project Structure
+
+Inside of your Astro + Starlight project, you'll see the following folders and files:
+
+```
+.
+โโโ public/
+โโโ src/
+โ โโโ assets/
+โ โโโ content/
+โ โ โโโ docs/
+โ โโโ content.config.ts
+โโโ astro.config.mjs
+โโโ package.json
+โโโ tailwind.config.mjs
+โโโ tsconfig.json
+```
+
+Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
+
+Images can be added to `src/assets/` and embedded in Markdown with a relative link.
+
+Static assets, like favicons, can be placed in the `public/` directory.
+
+## ๐ง Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## ๐ Want to learn more?
+
+Check out [Starlightโs docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
new file mode 100644
index 00000000..c6cc611f
--- /dev/null
+++ b/docs/astro.config.mjs
@@ -0,0 +1,36 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+import tailwind from '@astrojs/tailwind';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [
+ starlight({
+ title: 'NodeKit',
+ logo: {
+ src: './public/nodekit.png',
+ replacesTitle: true,
+ },
+ social: {
+ github: 'https://github.com/withastro/starlight',
+ },
+ sidebar: [
+ {
+ label: 'Guides',
+ items: [
+ // Each item here is one entry in the navigation menu.
+ { label: 'Example Guide', slug: 'guides/example' },
+ ],
+ },
+ {
+ label: 'Reference',
+ collapsed: true,
+ autogenerate: { directory: 'reference' },
+ },
+ ],
+ customCss: ['./src/tailwind.css'],
+ }),
+ tailwind({ applyBaseStyles: true }),
+ ],
+});
diff --git a/docs/package-lock.json b/docs/package-lock.json
new file mode 100644
index 00000000..2ae3eca0
--- /dev/null
+++ b/docs/package-lock.json
@@ -0,0 +1,8394 @@
+{
+ "name": "docs",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "docs",
+ "version": "0.0.1",
+ "dependencies": {
+ "@astrojs/starlight": "^0.30.3",
+ "@astrojs/starlight-tailwind": "^3.0.0",
+ "@astrojs/tailwind": "^5.1.3",
+ "astro": "^5.0.2",
+ "sharp": "^0.32.5",
+ "tailwindcss": "^3.4.4"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@astrojs/compiler": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.10.3.tgz",
+ "integrity": "sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw==",
+ "license": "MIT"
+ },
+ "node_modules/@astrojs/internal-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.4.2.tgz",
+ "integrity": "sha512-EdDWkC3JJVcpGpqJAU/5hSk2LKXyG3mNGkzGoAuyK+xoPHbaVdSuIWoN1QTnmK3N/gGfaaAfM8gO2KDCAW7S3w==",
+ "license": "MIT"
+ },
+ "node_modules/@astrojs/markdown-remark": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-6.0.1.tgz",
+ "integrity": "sha512-CTSYijj25NfxgZi15TU3CwPwgyD1/7yA3FcdcNmB9p94nydupiUbrIiq3IqeTp2m5kCVzxbPZeC7fTwEOaNyGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@astrojs/prism": "3.2.0",
+ "github-slugger": "^2.0.0",
+ "hast-util-from-html": "^2.0.3",
+ "hast-util-to-text": "^4.0.2",
+ "import-meta-resolve": "^4.1.0",
+ "js-yaml": "^4.1.0",
+ "mdast-util-definitions": "^6.0.0",
+ "rehype-raw": "^7.0.0",
+ "rehype-stringify": "^10.0.1",
+ "remark-gfm": "^4.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.1.1",
+ "remark-smartypants": "^3.0.2",
+ "shiki": "^1.23.1",
+ "unified": "^11.0.5",
+ "unist-util-remove-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "unist-util-visit-parents": "^6.0.1",
+ "vfile": "^6.0.3"
+ }
+ },
+ "node_modules/@astrojs/mdx": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-4.0.3.tgz",
+ "integrity": "sha512-8HcuyNG/KgYUAQWVzKFkboXcTOBCW6aQ0WK0Er/iSmVSF0y3yimg4/3QSt+Twv9dogpwIHL+E8iBJKqieFv4+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@astrojs/markdown-remark": "6.0.1",
+ "@mdx-js/mdx": "^3.1.0",
+ "acorn": "^8.14.0",
+ "es-module-lexer": "^1.5.4",
+ "estree-util-visit": "^2.0.0",
+ "hast-util-to-html": "^9.0.3",
+ "kleur": "^4.1.5",
+ "rehype-raw": "^7.0.0",
+ "remark-gfm": "^4.0.0",
+ "remark-smartypants": "^3.0.2",
+ "source-map": "^0.7.4",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.3"
+ },
+ "engines": {
+ "node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "astro": "^5.0.0"
+ }
+ },
+ "node_modules/@astrojs/prism": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.2.0.tgz",
+ "integrity": "sha512-GilTHKGCW6HMq7y3BUv9Ac7GMe/MO9gi9GW62GzKtth0SwukCu/qp2wLiGpEujhY+VVhaG9v7kv/5vFzvf4NYw==",
+ "license": "MIT",
+ "dependencies": {
+ "prismjs": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+ }
+ },
+ "node_modules/@astrojs/sitemap": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.2.1.tgz",
+ "integrity": "sha512-uxMfO8f7pALq0ADL6Lk68UV6dNYjJ2xGUzyjjVj60JLBs5a6smtlkBYv3tQ0DzoqwS7c9n4FUx5lgv0yPo/fgA==",
+ "license": "MIT",
+ "dependencies": {
+ "sitemap": "^8.0.0",
+ "stream-replace-string": "^2.0.0",
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@astrojs/starlight": {
+ "version": "0.30.3",
+ "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.30.3.tgz",
+ "integrity": "sha512-HbGYYIR2Rnrvvc2jD0dUpp8zUzv3jQYtG5im3aulDgE4Jo21Ahw0yXlb/Y134G3LALLbqhImmlbt/h/nDV3yMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@astrojs/mdx": "^4.0.1",
+ "@astrojs/sitemap": "^3.1.6",
+ "@pagefind/default-ui": "^1.0.3",
+ "@types/hast": "^3.0.4",
+ "@types/js-yaml": "^4.0.9",
+ "@types/mdast": "^4.0.4",
+ "astro-expressive-code": "^0.38.3",
+ "bcp-47": "^2.1.0",
+ "hast-util-from-html": "^2.0.1",
+ "hast-util-select": "^6.0.2",
+ "hast-util-to-string": "^3.0.0",
+ "hastscript": "^9.0.0",
+ "i18next": "^23.11.5",
+ "js-yaml": "^4.1.0",
+ "mdast-util-directive": "^3.0.0",
+ "mdast-util-to-markdown": "^2.1.0",
+ "mdast-util-to-string": "^4.0.0",
+ "pagefind": "^1.0.3",
+ "rehype": "^13.0.1",
+ "rehype-format": "^5.0.0",
+ "remark-directive": "^3.0.0",
+ "unified": "^11.0.5",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.2"
+ },
+ "peerDependencies": {
+ "astro": "^5.0.0"
+ }
+ },
+ "node_modules/@astrojs/starlight-tailwind": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@astrojs/starlight-tailwind/-/starlight-tailwind-3.0.0.tgz",
+ "integrity": "sha512-oYHG9RY+VaOSeAhheVZfm9HDA892qvcQA82VT86POYmg1OsgBuWwdf1ZbofV8iq/z5kO06ajcSdzhPE8lhEx8g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@astrojs/starlight": ">=0.30.0",
+ "@astrojs/tailwind": "^5.1.3",
+ "tailwindcss": "^3.3.3"
+ }
+ },
+ "node_modules/@astrojs/tailwind": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-5.1.4.tgz",
+ "integrity": "sha512-EJ3uoTZZr0RYwTrVS2HgYN0+VbXvg7h87AtwpD5OzqS3GyMwRmzfOwHfORTxoWGQRrY9k/Fi+Awk60kwpvRL5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.49",
+ "postcss-load-config": "^4.0.2"
+ },
+ "peerDependencies": {
+ "astro": "^3.0.0 || ^4.0.0 || ^5.0.0",
+ "tailwindcss": "^3.0.24"
+ }
+ },
+ "node_modules/@astrojs/telemetry": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.2.0.tgz",
+ "integrity": "sha512-wxhSKRfKugLwLlr4OFfcqovk+LIFtKwLyGPqMsv+9/ibqqnW3Gv7tBhtKEb0gAyUAC4G9BTVQeQahqnQAhd6IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^4.1.0",
+ "debug": "^4.3.7",
+ "dlv": "^1.1.3",
+ "dset": "^3.1.4",
+ "is-docker": "^3.0.0",
+ "is-wsl": "^3.1.0",
+ "which-pm-runs": "^1.1.0"
+ },
+ "engines": {
+ "node": "^18.17.1 || ^20.3.0 || >=22.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
+ "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+ "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.3",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
+ "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz",
+ "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
+ "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@expressive-code/core": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.38.3.tgz",
+ "integrity": "sha512-s0/OtdRpBONwcn23O8nVwDNQqpBGKscysejkeBkwlIeHRLZWgiTVrusT5Idrdz1d8cW5wRk9iGsAIQmwDPXgJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^4.0.4",
+ "hast-util-select": "^6.0.2",
+ "hast-util-to-html": "^9.0.1",
+ "hast-util-to-text": "^4.0.1",
+ "hastscript": "^9.0.0",
+ "postcss": "^8.4.38",
+ "postcss-nested": "^6.0.1",
+ "unist-util-visit": "^5.0.0",
+ "unist-util-visit-parents": "^6.0.1"
+ }
+ },
+ "node_modules/@expressive-code/plugin-frames": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.38.3.tgz",
+ "integrity": "sha512-qL2oC6FplmHNQfZ8ZkTR64/wKo9x0c8uP2WDftR/ydwN/yhe1ed7ZWYb8r3dezxsls+tDokCnN4zYR594jbpvg==",
+ "license": "MIT",
+ "dependencies": {
+ "@expressive-code/core": "^0.38.3"
+ }
+ },
+ "node_modules/@expressive-code/plugin-shiki": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.38.3.tgz",
+ "integrity": "sha512-kqHnglZeesqG3UKrb6e9Fq5W36AZ05Y9tCREmSN2lw8LVTqENIeCIkLDdWtQ5VoHlKqwUEQFTVlRehdwoY7Gmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@expressive-code/core": "^0.38.3",
+ "shiki": "^1.22.2"
+ }
+ },
+ "node_modules/@expressive-code/plugin-text-markers": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.38.3.tgz",
+ "integrity": "sha512-dPK3+BVGTbTmGQGU3Fkj3jZ3OltWUAlxetMHI6limUGCWBCucZiwoZeFM/WmqQa71GyKRzhBT+iEov6kkz2xVA==",
+ "license": "MIT",
+ "dependencies": {
+ "@expressive-code/core": "^0.38.3"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mdx-js/mdx": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz",
+ "integrity": "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdx": "^2.0.0",
+ "collapse-white-space": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "estree-util-scope": "^1.0.0",
+ "estree-walker": "^3.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "markdown-extensions": "^2.0.0",
+ "recma-build-jsx": "^1.0.0",
+ "recma-jsx": "^1.0.0",
+ "recma-stringify": "^1.0.0",
+ "rehype-recma": "^1.0.0",
+ "remark-mdx": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "source-map": "^0.7.0",
+ "unified": "^11.0.0",
+ "unist-util-position-from-estree": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@oslojs/encoding": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
+ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
+ "license": "MIT"
+ },
+ "node_modules/@pagefind/darwin-arm64": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.3.0.tgz",
+ "integrity": "sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@pagefind/darwin-x64": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.3.0.tgz",
+ "integrity": "sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@pagefind/default-ui": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.3.0.tgz",
+ "integrity": "sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg==",
+ "license": "MIT"
+ },
+ "node_modules/@pagefind/linux-arm64": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.3.0.tgz",
+ "integrity": "sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@pagefind/linux-x64": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.3.0.tgz",
+ "integrity": "sha512-hAvqdPJv7A20Ucb6FQGE6jhjqy+vZ6pf+s2tFMNtMBG+fzcdc91uTw7aP/1Vo5plD0dAOHwdxfkyw0ugal4kcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@pagefind/windows-x64": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.3.0.tgz",
+ "integrity": "sha512-BR1bIRWOMqkf8IoU576YDhij1Wd/Zf2kX/kCI0b2qzCKC8wcc2GQJaaRMCpzvCCrmliO4vtJ6RITp/AnoYUUmQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
+ "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.0.tgz",
+ "integrity": "sha512-qFcFto9figFLz2g25DxJ1WWL9+c91fTxnGuwhToCl8BaqDsDYMl/kOnBXAyAqkkzAWimYMSWNPWEjt+ADAHuoQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.0.tgz",
+ "integrity": "sha512-vqrQdusvVl7dthqNjWCL043qelBK+gv9v3ZiqdxgaJvmZyIAAXMjeGVSqZynKq69T7062T5VrVTuikKSAAVP6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.0.tgz",
+ "integrity": "sha512-617pd92LhdA9+wpixnzsyhVft3szYiN16aNUMzVkf2N+yAk8UXY226Bfp36LvxYTUt7MO/ycqGFjQgJ0wlMaWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.0.tgz",
+ "integrity": "sha512-Y3b4oDoaEhCypg8ajPqigKDcpi5ZZovemQl9Edpem0uNv6UUjXv7iySBpGIUTSs2ovWOzYpfw9EbFJXF/fJHWw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.0.tgz",
+ "integrity": "sha512-3REQJ4f90sFIBfa0BUokiCdrV/E4uIjhkWe1bMgCkhFXbf4D8YN6C4zwJL881GM818qVYE9BO3dGwjKhpo2ABA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.0.tgz",
+ "integrity": "sha512-ZtY3Y8icbe3Cc+uQicsXG5L+CRGUfLZjW6j2gn5ikpltt3Whqjfo5mkyZ86UiuHF9Q3ZsaQeW7YswlHnN+lAcg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.0.tgz",
+ "integrity": "sha512-bsPGGzfiHXMhQGuFGpmo2PyTwcrh2otL6ycSZAFTESviUoBOuxF7iBbAL5IJXc/69peXl5rAtbewBFeASZ9O0g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.0.tgz",
+ "integrity": "sha512-kvyIECEhs2DrrdfQf++maCWJIQ974EI4txlz1nNSBaCdtf7i5Xf1AQCEJWOC5rEBisdaMFFnOWNLYt7KpFqy5A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.0.tgz",
+ "integrity": "sha512-CFE7zDNrokaotXu+shwIrmWrFxllg79vciH4E/zeK7NitVuWEaXRzS0mFfFvyhZfn8WfVOG/1E9u8/DFEgK7WQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.0.tgz",
+ "integrity": "sha512-MctNTBlvMcIBP0t8lV/NXiUwFg9oK5F79CxLU+a3xgrdJjfBLVIEHSAjQ9+ipofN2GKaMLnFFXLltg1HEEPaGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.0.tgz",
+ "integrity": "sha512-fBpoYwLEPivL3q368+gwn4qnYnr7GVwM6NnMo8rJ4wb0p/Y5lg88vQRRP077gf+tc25akuqd+1Sxbn9meODhwA==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.0.tgz",
+ "integrity": "sha512-1hiHPV6dUaqIMXrIjN+vgJqtfkLpqHS1Xsg0oUfUVD98xGp1wX89PIXgDF2DWra1nxAd8dfE0Dk59MyeKaBVAw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.0.tgz",
+ "integrity": "sha512-U0xcC80SMpEbvvLw92emHrNjlS3OXjAM0aVzlWfar6PR0ODWCTQtKeeB+tlAPGfZQXicv1SpWwRz9Hyzq3Jx3g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.0.tgz",
+ "integrity": "sha512-VU/P/IODrNPasgZDLIFJmMiLGez+BN11DQWfTVlViJVabyF3JaeaJkP6teI8760f18BMGCQOW9gOmuzFaI1pUw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.0.tgz",
+ "integrity": "sha512-laQVRvdbKmjXuFA3ZiZj7+U24FcmoPlXEi2OyLfbpY2MW1oxLt9Au8q9eHd0x6Pw/Kw4oe9gwVXWwIf2PVqblg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.0.tgz",
+ "integrity": "sha512-3wzKzduS7jzxqcOvy/ocU/gMR3/QrHEFLge5CD7Si9fyHuoXcidyYZ6jyx8OPYmCcGm3uKTUl+9jUSAY74Ln5A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.0.tgz",
+ "integrity": "sha512-jROwnI1+wPyuv696rAFHp5+6RFhXGGwgmgSfzE8e4xfit6oLRg7GyMArVUoM3ChS045OwWr9aTnU+2c1UdBMyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.0.tgz",
+ "integrity": "sha512-duzweyup5WELhcXx5H1jokpr13i3BV9b48FMiikYAwk/MT1LrMYYk2TzenBd0jj4ivQIt58JWSxc19y4SvLP4g==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.0.tgz",
+ "integrity": "sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@shikijs/core": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.26.1.tgz",
+ "integrity": "sha512-yeo7sG+WZQblKPclUOKRPwkv1PyoHYkJ4gP9DzhFJbTdueKR7wYTI1vfF/bFi1NTgc545yG/DzvVhZgueVOXMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/engine-javascript": "1.26.1",
+ "@shikijs/engine-oniguruma": "1.26.1",
+ "@shikijs/types": "1.26.1",
+ "@shikijs/vscode-textmate": "^10.0.1",
+ "@types/hast": "^3.0.4",
+ "hast-util-to-html": "^9.0.4"
+ }
+ },
+ "node_modules/@shikijs/engine-javascript": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.26.1.tgz",
+ "integrity": "sha512-CRhA0b8CaSLxS0E9A4Bzcb3LKBNpykfo9F85ozlNyArxjo2NkijtiwrJZ6eHa+NT5I9Kox2IXVdjUsP4dilsmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "1.26.1",
+ "@shikijs/vscode-textmate": "^10.0.1",
+ "oniguruma-to-es": "0.10.0"
+ }
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.1.tgz",
+ "integrity": "sha512-F5XuxN1HljLuvfXv7d+mlTkV7XukC1cawdtOo+7pKgPD83CAB1Sf8uHqP3PK0u7njFH0ZhoXE1r+0JzEgAQ+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "1.26.1",
+ "@shikijs/vscode-textmate": "^10.0.1"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.26.1.tgz",
+ "integrity": "sha512-oz/TQiIqZejEIZbGtn68hbJijAOTtYH4TMMSWkWYozwqdpKR3EXgILneQy26WItmJjp3xVspHdiUxUCws4gtuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "1.26.1"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.26.1.tgz",
+ "integrity": "sha512-JDxVn+z+wgLCiUhBGx2OQrLCkKZQGzNH3nAxFir4PjUcYiyD8Jdms9izyxIogYmSwmoPTatFTdzyrRKbKlSfPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "1.26.1"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.26.1.tgz",
+ "integrity": "sha512-d4B00TKKAMaHuFYgRf3L0gwtvqpW4hVdVwKcZYbBfAAQXspgkbWqnFfuFl3MDH6gLbsubOcr+prcnsqah3ny7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.1",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/@shikijs/vscode-textmate": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz",
+ "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/acorn": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
+ "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/js-yaml": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
+ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdx": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz",
+ "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ms": {
+ "version": "0.7.34",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
+ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/nlcst": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz",
+ "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.10.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
+ "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/@types/sax": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
+ "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz",
+ "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==",
+ "license": "ISC"
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ansi-align": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
+ "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "node_modules/ansi-align/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-align/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-align/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-align/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/array-iterate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz",
+ "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/astring": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
+ "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
+ "license": "MIT",
+ "bin": {
+ "astring": "bin/astring"
+ }
+ },
+ "node_modules/astro": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/astro/-/astro-5.1.3.tgz",
+ "integrity": "sha512-Zl/B4hmueJmlI5FJQcwbBRhHbRvbTWaTJzimkbH+jYczR5blt4uSh0DheyAt/+NwcnGfiC11z/XXJYIBR4LPUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@astrojs/compiler": "^2.10.3",
+ "@astrojs/internal-helpers": "0.4.2",
+ "@astrojs/markdown-remark": "6.0.1",
+ "@astrojs/telemetry": "3.2.0",
+ "@oslojs/encoding": "^1.1.0",
+ "@rollup/pluginutils": "^5.1.3",
+ "@types/cookie": "^0.6.0",
+ "acorn": "^8.14.0",
+ "aria-query": "^5.3.2",
+ "axobject-query": "^4.1.0",
+ "boxen": "8.0.1",
+ "ci-info": "^4.1.0",
+ "clsx": "^2.1.1",
+ "common-ancestor-path": "^1.0.1",
+ "cookie": "^0.7.2",
+ "cssesc": "^3.0.0",
+ "debug": "^4.3.7",
+ "deterministic-object-hash": "^2.0.2",
+ "devalue": "^5.1.1",
+ "diff": "^5.2.0",
+ "dlv": "^1.1.3",
+ "dset": "^3.1.4",
+ "es-module-lexer": "^1.5.4",
+ "esbuild": "^0.21.5",
+ "estree-walker": "^3.0.3",
+ "fast-glob": "^3.3.2",
+ "flattie": "^1.1.1",
+ "github-slugger": "^2.0.0",
+ "html-escaper": "^3.0.3",
+ "http-cache-semantics": "^4.1.1",
+ "js-yaml": "^4.1.0",
+ "kleur": "^4.1.5",
+ "magic-string": "^0.30.14",
+ "magicast": "^0.3.5",
+ "micromatch": "^4.0.8",
+ "mrmime": "^2.0.0",
+ "neotraverse": "^0.6.18",
+ "p-limit": "^6.1.0",
+ "p-queue": "^8.0.1",
+ "preferred-pm": "^4.0.0",
+ "prompts": "^2.4.2",
+ "rehype": "^13.0.2",
+ "semver": "^7.6.3",
+ "shiki": "^1.23.1",
+ "tinyexec": "^0.3.1",
+ "tsconfck": "^3.1.4",
+ "ultrahtml": "^1.5.3",
+ "unist-util-visit": "^5.0.0",
+ "unstorage": "^1.14.0",
+ "vfile": "^6.0.3",
+ "vite": "^6.0.5",
+ "vitefu": "^1.0.4",
+ "which-pm": "^3.0.0",
+ "xxhash-wasm": "^1.1.0",
+ "yargs-parser": "^21.1.1",
+ "yocto-spinner": "^0.1.0",
+ "zod": "^3.23.8",
+ "zod-to-json-schema": "^3.23.5",
+ "zod-to-ts": "^1.2.0"
+ },
+ "bin": {
+ "astro": "astro.js"
+ },
+ "engines": {
+ "node": "^18.17.1 || ^20.3.0 || >=22.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0"
+ },
+ "optionalDependencies": {
+ "sharp": "^0.33.3"
+ }
+ },
+ "node_modules/astro-expressive-code": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.38.3.tgz",
+ "integrity": "sha512-Tvdc7RV0G92BbtyEOsfJtXU35w41CkM94fOAzxbQP67Wj5jArfserJ321FO4XA7WG9QMV0GIBmQq77NBIRDzpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "rehype-expressive-code": "^0.38.3"
+ },
+ "peerDependencies": {
+ "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0"
+ }
+ },
+ "node_modules/astro/node_modules/sharp": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.20",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+ "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.23.3",
+ "caniuse-lite": "^1.0.30001646",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/bare-events": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.2.tgz",
+ "integrity": "sha512-KSdMqLj1ZERZMP1PTmnLK7SqJu9z9/SbwUUPZly2puMtfVcytC+jl6mb/9XYiqq0PXcx1rNDS+Qvl1g54Lho6A==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-fs": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
+ "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.0.0",
+ "bare-path": "^2.0.0",
+ "bare-stream": "^2.0.0"
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
+ "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
+ "license": "Apache-2.0",
+ "optional": true
+ },
+ "node_modules/bare-path": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
+ "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^2.1.0"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.1.tgz",
+ "integrity": "sha512-eVZbtKM+4uehzrsj49KtCy3Pbg7kO1pJ3SKZ1SFrIH/0pnj9scuGGgUlNDf/7qS8WKtGdiJY5Kyhs/ivYPTB/g==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.21.0"
+ }
+ },
+ "node_modules/base-64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
+ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bcp-47": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
+ "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47-match": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
+ "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
+ },
+ "node_modules/boxen": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
+ "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-align": "^3.0.1",
+ "camelcase": "^8.0.0",
+ "chalk": "^5.3.0",
+ "cli-boxes": "^3.0.0",
+ "string-width": "^7.2.0",
+ "type-fest": "^4.21.0",
+ "widest-line": "^5.0.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+ "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001688",
+ "electron-to-chromium": "^1.5.73",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.1"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
+ "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001690",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
+ "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/ci-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
+ "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-boxes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
+ "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/collapse-white-space": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
+ "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/common-ancestor-path": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz",
+ "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==",
+ "license": "ISC"
+ },
+ "node_modules/consola": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.3.3.tgz",
+ "integrity": "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-es": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
+ "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crossws": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz",
+ "integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==",
+ "license": "MIT",
+ "dependencies": {
+ "uncrypto": "^0.1.3"
+ }
+ },
+ "node_modules/css-selector-parser": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.0.5.tgz",
+ "integrity": "sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
+ "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/destr": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz",
+ "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+ "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/deterministic-object-hash": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz",
+ "integrity": "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base-64": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/devalue": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
+ "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/direction": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz",
+ "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==",
+ "license": "MIT",
+ "bin": {
+ "direction": "cli.js"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/dset": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
+ "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.77",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.77.tgz",
+ "integrity": "sha512-AnJSrt5JpRVgY6dgd5yccguLc5A7oMSF0Kt3fcW+Hp5WTuFbl5upeSFZbMZYy2o7jhmIhU8Ekrd82GhyXUqUUg==",
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex-xs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
+ "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
+ "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
+ "license": "MIT"
+ },
+ "node_modules/esast-util-from-estree": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
+ "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-visit": "^2.0.0",
+ "unist-util-position-from-estree": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/esast-util-from-js": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz",
+ "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "acorn": "^8.0.0",
+ "esast-util-from-estree": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estree-util-attach-comments": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
+ "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-build-jsx": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz",
+ "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "estree-walker": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-scope": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz",
+ "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "devlop": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-to-js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz",
+ "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "astring": "^1.8.0",
+ "source-map": "^0.7.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-util-visit": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz",
+ "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expressive-code": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.38.3.tgz",
+ "integrity": "sha512-COM04AiUotHCKJgWdn7NtW2lqu8OW8owAidMpkXt1qxrZ9Q2iC7+tok/1qIn2ocGnczvr9paIySgGnEwFeEQ8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@expressive-code/core": "^0.38.3",
+ "@expressive-code/plugin-frames": "^0.38.3",
+ "@expressive-code/plugin-shiki": "^0.38.3",
+ "@expressive-code/plugin-text-markers": "^0.38.3"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
+ "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up-simple": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz",
+ "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-yarn-workspace-root2": {
+ "version": "1.2.16",
+ "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz",
+ "integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "micromatch": "^4.0.2",
+ "pkg-dir": "^4.2.0"
+ }
+ },
+ "node_modules/flattie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
+ "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
+ "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/github-slugger": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
+ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
+ "license": "ISC"
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
+ "node_modules/h3": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz",
+ "integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie-es": "^1.2.2",
+ "crossws": ">=0.2.0 <0.4.0",
+ "defu": "^6.1.4",
+ "destr": "^2.0.3",
+ "iron-webcrypto": "^1.2.1",
+ "ohash": "^1.1.4",
+ "radix3": "^1.1.2",
+ "ufo": "^1.5.4",
+ "uncrypto": "^0.1.3",
+ "unenv": "^1.10.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hast-util-embedded": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz",
+ "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-is-element": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-format": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz",
+ "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-embedded": "^3.0.0",
+ "hast-util-minify-whitespace": "^1.0.0",
+ "hast-util-phrasing": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-whitespace-sensitive-tag-names": "^3.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-html": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
+ "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "devlop": "^1.1.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "parse5": "^7.0.0",
+ "vfile": "^6.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-parse5": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.2.tgz",
+ "integrity": "sha512-SfMzfdAi/zAoZ1KkFEyyeXBn7u/ShQrfd675ZEE9M3qj+PMFX05xubzRyF76CCSJu8au9jgVxDV1+okFvgZU4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "hastscript": "^9.0.0",
+ "property-information": "^6.0.0",
+ "vfile": "^6.0.0",
+ "vfile-location": "^5.0.0",
+ "web-namespaces": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-has-property": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz",
+ "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-is-body-ok-link": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz",
+ "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-is-element": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
+ "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-minify-whitespace": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz",
+ "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-embedded": "^3.0.0",
+ "hast-util-is-element": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-parse-selector": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
+ "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-phrasing": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz",
+ "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-embedded": "^3.0.0",
+ "hast-util-has-property": "^3.0.0",
+ "hast-util-is-body-ok-link": "^3.0.0",
+ "hast-util-is-element": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-raw": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
+ "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "hast-util-from-parse5": "^8.0.0",
+ "hast-util-to-parse5": "^8.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "parse5": "^7.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-select": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.3.tgz",
+ "integrity": "sha512-OVRQlQ1XuuLP8aFVLYmC2atrfWHS5UD3shonxpnyrjcCkwtvmt/+N6kYJdcY4mkMJhxp4kj2EFIxQ9kvkkt/eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "bcp-47-match": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "css-selector-parser": "^3.0.0",
+ "devlop": "^1.0.0",
+ "direction": "^2.0.0",
+ "hast-util-has-property": "^3.0.0",
+ "hast-util-to-string": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "nth-check": "^2.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-estree": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.1.tgz",
+ "integrity": "sha512-IWtwwmPskfSmma9RpzCappDUitC8t5jhAynHhc1m2+5trOgsrp7txscUSavc5Ic8PATyAjfrCK1wgtxh2cICVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-attach-comments": "^3.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-object": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-html": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz",
+ "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "stringify-entities": "^4.0.0",
+ "zwitch": "^2.0.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz",
+ "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-object": "^1.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-parse5": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz",
+ "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "web-namespaces": "^2.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-string": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
+ "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-to-text": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
+ "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "hast-util-is-element": "^3.0.0",
+ "unist-util-find-after": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hastscript": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.0.tgz",
+ "integrity": "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^4.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/html-whitespace-sensitive-tag-names": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz",
+ "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/i18next": {
+ "version": "23.16.8",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
+ "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
+ "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
+ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
+ "license": "MIT"
+ },
+ "node_modules/iron-webcrypto": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
+ "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/brc-dd"
+ }
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/load-yaml-file": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz",
+ "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.5",
+ "js-yaml": "^3.13.0",
+ "pify": "^4.0.1",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/load-yaml-file/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/load-yaml-file/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/load-yaml-file/node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/markdown-extensions": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
+ "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/markdown-table": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
+ "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdast-util-definitions": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz",
+ "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "unist-util-visit": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-directive": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz",
+ "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-find-and-replace": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
+ "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "escape-string-regexp": "^5.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz",
+ "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-gfm-autolink-literal": "^2.0.0",
+ "mdast-util-gfm-footnote": "^2.0.0",
+ "mdast-util-gfm-strikethrough": "^2.0.0",
+ "mdast-util-gfm-table": "^2.0.0",
+ "mdast-util-gfm-task-list-item": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-autolink-literal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
+ "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-find-and-replace": "^3.0.0",
+ "micromark-util-character": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-footnote": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz",
+ "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-strikethrough": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
+ "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-table": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
+ "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "markdown-table": "^3.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-gfm-task-list-item": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
+ "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
+ "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz",
+ "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
+ "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz",
+ "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz",
+ "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-directive": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz",
+ "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "parse-entities": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
+ "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-extension-gfm-autolink-literal": "^2.0.0",
+ "micromark-extension-gfm-footnote": "^2.0.0",
+ "micromark-extension-gfm-strikethrough": "^2.0.0",
+ "micromark-extension-gfm-table": "^2.0.0",
+ "micromark-extension-gfm-tagfilter": "^2.0.0",
+ "micromark-extension-gfm-task-list-item": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-autolink-literal": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
+ "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-footnote": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
+ "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-strikethrough": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
+ "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-table": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz",
+ "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-tagfilter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
+ "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-gfm-task-list-item": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
+ "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdx-expression": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz",
+ "integrity": "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-mdx-expression": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-extension-mdx-jsx": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.1.tgz",
+ "integrity": "sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/acorn": "^4.0.0",
+ "@types/estree": "^1.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "micromark-factory-mdx-expression": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdx-md": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz",
+ "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdxjs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz",
+ "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.0.0",
+ "acorn-jsx": "^5.0.0",
+ "micromark-extension-mdx-expression": "^3.0.0",
+ "micromark-extension-mdx-jsx": "^3.0.0",
+ "micromark-extension-mdx-md": "^2.0.0",
+ "micromark-extension-mdxjs-esm": "^3.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-extension-mdxjs-esm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz",
+ "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-position-from-estree": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-mdx-expression": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.2.tgz",
+ "integrity": "sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-events-to-acorn": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-position-from-estree": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-events-to-acorn": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz",
+ "integrity": "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/acorn": "^4.0.0",
+ "@types/estree": "^1.0.0",
+ "@types/unist": "^3.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-visit": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "vfile-message": "^4.0.0"
+ }
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.3.tgz",
+ "integrity": "sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz",
+ "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
+ "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
+ "license": "MIT"
+ },
+ "node_modules/neotraverse": {
+ "version": "0.6.18",
+ "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz",
+ "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/nlcst-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz",
+ "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/nlcst": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.71.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz",
+ "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch-native": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz",
+ "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==",
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ofetch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz",
+ "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==",
+ "license": "MIT",
+ "dependencies": {
+ "destr": "^2.0.3",
+ "node-fetch-native": "^1.6.4",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz",
+ "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==",
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/oniguruma-to-es": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.10.0.tgz",
+ "integrity": "sha512-zapyOUOCJxt+xhiNRPPMtfJkHGsZ98HHB9qJEkdT8BGytO/+kpe4m1Ngf0MzbzTmhacn11w9yGeDP6tzDhnCdg==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex-xs": "^1.0.0",
+ "regex": "^5.1.1",
+ "regex-recursion": "^5.1.1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz",
+ "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==",
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-queue": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.0.1.tgz",
+ "integrity": "sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^5.0.1",
+ "p-timeout": "^6.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
+ "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/pagefind": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.3.0.tgz",
+ "integrity": "sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==",
+ "license": "MIT",
+ "bin": {
+ "pagefind": "lib/runner/bin.cjs"
+ },
+ "optionalDependencies": {
+ "@pagefind/darwin-arm64": "1.3.0",
+ "@pagefind/darwin-x64": "1.3.0",
+ "@pagefind/linux-arm64": "1.3.0",
+ "@pagefind/linux-x64": "1.3.0",
+ "@pagefind/windows-x64": "1.3.0"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/parse-latin": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
+ "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/nlcst": "^2.0.0",
+ "@types/unist": "^3.0.0",
+ "nlcst-to-string": "^4.0.0",
+ "unist-util-modify-children": "^4.0.0",
+ "unist-util-visit-children": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
+ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^4.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
+ "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/preferred-pm": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-4.0.0.tgz",
+ "integrity": "sha512-gYBeFTZLu055D8Vv3cSPox/0iTPtkzxpLroSYYA7WXgRi31WCJ51Uyl8ZiPeUUjyvs2MBzK+S8v9JVUgHU/Sqw==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up-simple": "^1.0.0",
+ "find-yarn-workspace-root2": "1.2.16",
+ "which-pm": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18.12"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
+ "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prompts/node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
+ "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
+ "license": "MIT"
+ },
+ "node_modules/radix3": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
+ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
+ "license": "MIT"
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/recma-build-jsx": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz",
+ "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-util-build-jsx": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/recma-jsx": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz",
+ "integrity": "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn-jsx": "^5.0.0",
+ "estree-util-to-js": "^2.0.0",
+ "recma-parse": "^1.0.0",
+ "recma-stringify": "^1.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/recma-parse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz",
+ "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "esast-util-from-js": "^2.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/recma-stringify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz",
+ "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-util-to-js": "^2.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "license": "MIT"
+ },
+ "node_modules/regex": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz",
+ "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-recursion": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz",
+ "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==",
+ "license": "MIT",
+ "dependencies": {
+ "regex": "^5.1.1",
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-utilities": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
+ "license": "MIT"
+ },
+ "node_modules/rehype": {
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz",
+ "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "rehype-parse": "^9.0.0",
+ "rehype-stringify": "^10.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-expressive-code": {
+ "version": "0.38.3",
+ "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.38.3.tgz",
+ "integrity": "sha512-RYSSDkMBikoTbycZPkcWp6ELneANT4eTpND1DSRJ6nI2eVFUwTBDCvE2vO6jOOTaavwnPiydi4i/87NRyjpdOA==",
+ "license": "MIT",
+ "dependencies": {
+ "expressive-code": "^0.38.3"
+ }
+ },
+ "node_modules/rehype-format": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz",
+ "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-format": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-parse": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
+ "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-from-html": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-raw": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
+ "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-raw": "^9.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-recma": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz",
+ "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "hast-util-to-estree": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/rehype-stringify": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz",
+ "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "hast-util-to-html": "^9.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-directive": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz",
+ "integrity": "sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-directive": "^3.0.0",
+ "micromark-extension-directive": "^3.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-gfm": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz",
+ "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-gfm": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-stringify": "^11.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-mdx": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz",
+ "integrity": "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdast-util-mdx": "^3.0.0",
+ "micromark-extension-mdxjs": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz",
+ "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-smartypants": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz",
+ "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==",
+ "license": "MIT",
+ "dependencies": {
+ "retext": "^9.0.0",
+ "retext-smartypants": "^6.0.0",
+ "unified": "^11.0.4",
+ "unist-util-visit": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/remark-stringify": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/retext": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz",
+ "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/nlcst": "^2.0.0",
+ "retext-latin": "^4.0.0",
+ "retext-stringify": "^4.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/retext-latin": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz",
+ "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/nlcst": "^2.0.0",
+ "parse-latin": "^7.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/retext-smartypants": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz",
+ "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/nlcst": "^2.0.0",
+ "nlcst-to-string": "^4.0.0",
+ "unist-util-visit": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/retext-stringify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz",
+ "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/nlcst": "^2.0.0",
+ "nlcst-to-string": "^4.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.30.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.0.tgz",
+ "integrity": "sha512-sDnr1pcjTgUT69qBksNF1N1anwfbyYG6TBQ22b03bII8EdiUQ7J0TlozVaTMjT/eEJAO49e1ndV7t+UZfL1+vA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.30.0",
+ "@rollup/rollup-android-arm64": "4.30.0",
+ "@rollup/rollup-darwin-arm64": "4.30.0",
+ "@rollup/rollup-darwin-x64": "4.30.0",
+ "@rollup/rollup-freebsd-arm64": "4.30.0",
+ "@rollup/rollup-freebsd-x64": "4.30.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.30.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.30.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.30.0",
+ "@rollup/rollup-linux-arm64-musl": "4.30.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.30.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.30.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.30.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.30.0",
+ "@rollup/rollup-linux-x64-gnu": "4.30.0",
+ "@rollup/rollup-linux-x64-musl": "4.30.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.30.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.30.0",
+ "@rollup/rollup-win32-x64-msvc": "4.30.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "license": "ISC"
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shiki": {
+ "version": "1.26.1",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.26.1.tgz",
+ "integrity": "sha512-Gqg6DSTk3wYqaZ5OaYtzjcdxcBvX5kCy24yvRJEgjT5U+WHlmqCThLuBUx0juyxQBi+6ug53IGeuQS07DWwpcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "1.26.1",
+ "@shikijs/engine-javascript": "1.26.1",
+ "@shikijs/engine-oniguruma": "1.26.1",
+ "@shikijs/langs": "1.26.1",
+ "@shikijs/themes": "1.26.1",
+ "@shikijs/types": "1.26.1",
+ "@shikijs/vscode-textmate": "^10.0.1",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "license": "MIT"
+ },
+ "node_modules/sitemap": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.0.tgz",
+ "integrity": "sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^17.0.5",
+ "@types/sax": "^1.2.1",
+ "arg": "^5.0.0",
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "sitemap": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0",
+ "npm": ">=6.0.0"
+ }
+ },
+ "node_modules/sitemap/node_modules/@types/node": {
+ "version": "17.0.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
+ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
+ "license": "MIT"
+ },
+ "node_modules/source-map": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
+ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stream-replace-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz",
+ "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
+ "license": "MIT"
+ },
+ "node_modules/streamx": {
+ "version": "2.21.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz",
+ "integrity": "sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "queue-tick": "^1.0.1",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
+ "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.4"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
+ "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^2.1.1",
+ "bare-path": "^2.1.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
+ "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tsconfck": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz",
+ "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==",
+ "license": "MIT",
+ "bin": {
+ "tsconfck": "bin/tsconfck.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.31.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.31.0.tgz",
+ "integrity": "sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
+ "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
+ "license": "MIT"
+ },
+ "node_modules/ultrahtml": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz",
+ "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==",
+ "license": "MIT"
+ },
+ "node_modules/uncrypto": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
+ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "license": "MIT"
+ },
+ "node_modules/unenv": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz",
+ "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "consola": "^3.2.3",
+ "defu": "^6.1.4",
+ "mime": "^3.0.0",
+ "node-fetch-native": "^1.6.4",
+ "pathe": "^1.1.2"
+ }
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-find-after": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
+ "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
+ "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-modify-children": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz",
+ "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "array-iterate": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position-from-estree": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz",
+ "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-remove-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
+ "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-visit": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-children": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz",
+ "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
+ "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unstorage": {
+ "version": "1.14.4",
+ "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.14.4.tgz",
+ "integrity": "sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "^3.1.3",
+ "chokidar": "^3.6.0",
+ "destr": "^2.0.3",
+ "h3": "^1.13.0",
+ "lru-cache": "^10.4.3",
+ "node-fetch-native": "^1.6.4",
+ "ofetch": "^1.4.1",
+ "ufo": "^1.5.4"
+ },
+ "peerDependencies": {
+ "@azure/app-configuration": "^1.8.0",
+ "@azure/cosmos": "^4.2.0",
+ "@azure/data-tables": "^13.3.0",
+ "@azure/identity": "^4.5.0",
+ "@azure/keyvault-secrets": "^4.9.0",
+ "@azure/storage-blob": "^12.26.0",
+ "@capacitor/preferences": "^6.0.3",
+ "@deno/kv": ">=0.8.4",
+ "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0",
+ "@planetscale/database": "^1.19.0",
+ "@upstash/redis": "^1.34.3",
+ "@vercel/blob": ">=0.27.0",
+ "@vercel/kv": "^1.0.1",
+ "aws4fetch": "^1.0.20",
+ "db0": ">=0.2.1",
+ "idb-keyval": "^6.2.1",
+ "ioredis": "^5.4.2",
+ "uploadthing": "^7.4.1"
+ },
+ "peerDependenciesMeta": {
+ "@azure/app-configuration": {
+ "optional": true
+ },
+ "@azure/cosmos": {
+ "optional": true
+ },
+ "@azure/data-tables": {
+ "optional": true
+ },
+ "@azure/identity": {
+ "optional": true
+ },
+ "@azure/keyvault-secrets": {
+ "optional": true
+ },
+ "@azure/storage-blob": {
+ "optional": true
+ },
+ "@capacitor/preferences": {
+ "optional": true
+ },
+ "@deno/kv": {
+ "optional": true
+ },
+ "@netlify/blobs": {
+ "optional": true
+ },
+ "@planetscale/database": {
+ "optional": true
+ },
+ "@upstash/redis": {
+ "optional": true
+ },
+ "@vercel/blob": {
+ "optional": true
+ },
+ "@vercel/kv": {
+ "optional": true
+ },
+ "aws4fetch": {
+ "optional": true
+ },
+ "db0": {
+ "optional": true
+ },
+ "idb-keyval": {
+ "optional": true
+ },
+ "ioredis": {
+ "optional": true
+ },
+ "uploadthing": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+ "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-location": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
+ "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
+ "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.0.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
+ "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.24.2",
+ "postcss": "^8.4.49",
+ "rollup": "^4.23.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
+ "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
+ "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
+ "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/android-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
+ "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
+ "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/darwin-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
+ "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
+ "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
+ "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
+ "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
+ "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
+ "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-loong64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
+ "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
+ "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
+ "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
+ "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-s390x": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
+ "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/linux-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
+ "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
+ "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/sunos-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
+ "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-arm64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
+ "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-ia32": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
+ "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/@esbuild/win32-x64": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
+ "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/vite/node_modules/esbuild": {
+ "version": "0.24.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
+ "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.24.2",
+ "@esbuild/android-arm": "0.24.2",
+ "@esbuild/android-arm64": "0.24.2",
+ "@esbuild/android-x64": "0.24.2",
+ "@esbuild/darwin-arm64": "0.24.2",
+ "@esbuild/darwin-x64": "0.24.2",
+ "@esbuild/freebsd-arm64": "0.24.2",
+ "@esbuild/freebsd-x64": "0.24.2",
+ "@esbuild/linux-arm": "0.24.2",
+ "@esbuild/linux-arm64": "0.24.2",
+ "@esbuild/linux-ia32": "0.24.2",
+ "@esbuild/linux-loong64": "0.24.2",
+ "@esbuild/linux-mips64el": "0.24.2",
+ "@esbuild/linux-ppc64": "0.24.2",
+ "@esbuild/linux-riscv64": "0.24.2",
+ "@esbuild/linux-s390x": "0.24.2",
+ "@esbuild/linux-x64": "0.24.2",
+ "@esbuild/netbsd-arm64": "0.24.2",
+ "@esbuild/netbsd-x64": "0.24.2",
+ "@esbuild/openbsd-arm64": "0.24.2",
+ "@esbuild/openbsd-x64": "0.24.2",
+ "@esbuild/sunos-x64": "0.24.2",
+ "@esbuild/win32-arm64": "0.24.2",
+ "@esbuild/win32-ia32": "0.24.2",
+ "@esbuild/win32-x64": "0.24.2"
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz",
+ "integrity": "sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==",
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/web-namespaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
+ "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-pm": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-3.0.0.tgz",
+ "integrity": "sha512-ysVYmw6+ZBhx3+ZkcPwRuJi38ZOTLJJ33PSHaitLxSKUMsh0LkKd0nC69zZCwt5D+AYUcMK2hhw4yWny20vSGg==",
+ "license": "MIT",
+ "dependencies": {
+ "load-yaml-file": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=18.12"
+ }
+ },
+ "node_modules/which-pm-runs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz",
+ "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/widest-line": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
+ "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xxhash-wasm": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
+ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==",
+ "license": "MIT"
+ },
+ "node_modules/yaml": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
+ "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
+ "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yocto-spinner": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-0.1.2.tgz",
+ "integrity": "sha512-VfmLIh/ZSZOJnVRQZc/dvpPP90lWL4G0bmxQMP0+U/2vKBA8GSpcBuWv17y7F+CZItRuO97HN1wdbb4p10uhOg==",
+ "license": "MIT",
+ "dependencies": {
+ "yoctocolors": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=18.19"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
+ "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
+ "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ },
+ "node_modules/zod-to-ts": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz",
+ "integrity": "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==",
+ "peerDependencies": {
+ "typescript": "^4.9.4 || ^5.0.2",
+ "zod": "^3"
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 00000000..0fdf6625
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "docs",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/starlight": "^0.30.3",
+ "@astrojs/starlight-tailwind": "^3.0.0",
+ "@astrojs/tailwind": "^5.1.3",
+ "astro": "^5.0.2",
+ "sharp": "^0.32.5",
+ "tailwindcss": "^3.4.4"
+ }
+}
\ No newline at end of file
diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg
new file mode 100644
index 00000000..cba5ac14
--- /dev/null
+++ b/docs/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/public/nodekit.png b/docs/public/nodekit.png
new file mode 100644
index 00000000..bcf48d09
Binary files /dev/null and b/docs/public/nodekit.png differ
diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts
new file mode 100644
index 00000000..d9ee8c9d
--- /dev/null
+++ b/docs/src/content.config.ts
@@ -0,0 +1,7 @@
+import { defineCollection } from 'astro:content';
+import { docsLoader } from '@astrojs/starlight/loaders';
+import { docsSchema } from '@astrojs/starlight/schema';
+
+export const collections = {
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
+};
diff --git a/docs/src/content/docs/guides/example.md b/docs/src/content/docs/guides/example.md
new file mode 100644
index 00000000..ebd0f3bc
--- /dev/null
+++ b/docs/src/content/docs/guides/example.md
@@ -0,0 +1,11 @@
+---
+title: Example Guide
+description: A guide in my new Starlight docs site.
+---
+
+Guides lead a user through a specific task they want to accomplish, often with a sequence of steps.
+Writing a good guide requires thinking about what your users are trying to do.
+
+## Further reading
+
+- Read [about how-to guides](https://diataxis.fr/how-to-guides/) in the Diรกtaxis framework
diff --git a/docs/src/content/docs/reference/nodekit.md b/docs/src/content/docs/reference/nodekit.md
new file mode 100644
index 00000000..39f7447e
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit.md
@@ -0,0 +1,39 @@
+---
+title: "nodekit"
+slug: "reference/nodekit"
+---
+## Synopsis
+
+
+Manage Algorand nodes from the command line
+
+Overview:
+Welcome to NodeKit, a TUI for managing Algorand nodes.
+A one stop shop for managing Algorand nodes, including node creation, configuration, and management.
+
+Note: This is still a work in progress. Expect bugs and rough edges.
+
+```
+nodekit [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for nodekit
+```
+
+### SEE ALSO
+
+* [nodekit bootstrap](/reference/nodekit/bootstrap) - Initialize a fresh node
+* [nodekit catchup](/reference/nodekit/catchup) - Manage Fast-Catchup for your node
+* [nodekit configure](/reference/nodekit/configure) - Change settings on the system (WIP)
+* [nodekit debug](/reference/nodekit/debug) - Display debugging information
+* [nodekit install](/reference/nodekit/install) - Install the node daemon
+* [nodekit start](/reference/nodekit/start) - Start the node daemon
+* [nodekit stop](/reference/nodekit/stop) - Stop the node daemon
+* [nodekit uninstall](/reference/nodekit/uninstall) - Uninstall the node daemon
+* [nodekit upgrade](/reference/nodekit/upgrade) - Upgrade the node daemon
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_bootstrap.md b/docs/src/content/docs/reference/nodekit_bootstrap.md
new file mode 100644
index 00000000..727f9cd2
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_bootstrap.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit bootstrap"
+slug: "reference/nodekit/bootstrap"
+---
+## Synopsis
+
+
+Initialize a fresh node
+
+Overview:
+Get up and running with a fresh Algorand node.
+Uses the local package manager to install Algorand, and then starts the node and preforms a Fast-Catchup.
+
+Note: This command only supports the default data directory, /var/lib/algorand
+
+```
+nodekit bootstrap [flags]
+```
+
+### Options
+
+```
+ -h, --help help for bootstrap
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_catchup.md b/docs/src/content/docs/reference/nodekit_catchup.md
new file mode 100644
index 00000000..5679e76d
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_catchup.md
@@ -0,0 +1,34 @@
+---
+title: "nodekit catchup"
+slug: "reference/nodekit/catchup"
+---
+## Synopsis
+
+
+Fast-Catchup is a feature that allows your node to catch up to the network faster than normal.
+
+Overview:
+The entire process should sync a node in minutes rather than hours or days.
+Actual sync times may vary depending on the number of accounts, number of blocks and the network.
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for catchup
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+* [nodekit catchup debug](/reference/nodekit/catchup/debug) - Display debug information for Fast-Catchup.
+* [nodekit catchup start](/reference/nodekit/catchup/start) - Get the latest catchpoint and start catching up.
+* [nodekit catchup stop](/reference/nodekit/catchup/stop) - Stop a fast catchup
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_catchup_debug.md b/docs/src/content/docs/reference/nodekit_catchup_debug.md
new file mode 100644
index 00000000..5b459ac6
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_catchup_debug.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit catchup debug"
+slug: "reference/nodekit/catchup/debug"
+---
+## Synopsis
+
+
+Display debug information for Fast-Catchup.
+
+Overview:
+This information is useful for debugging fast-catchup issues.
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup debug [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for debug
+```
+
+### SEE ALSO
+
+* [nodekit catchup](/reference/nodekit/catchup) - Manage Fast-Catchup for your node
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_catchup_start.md b/docs/src/content/docs/reference/nodekit_catchup_start.md
new file mode 100644
index 00000000..01be034d
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_catchup_start.md
@@ -0,0 +1,31 @@
+---
+title: "nodekit catchup start"
+slug: "reference/nodekit/catchup/start"
+---
+## Synopsis
+
+
+Catchup the node to the latest catchpoint.
+
+Overview:
+Starting a catchup will sync the node to the latest catchpoint.
+Actual sync times may vary depending on the number of accounts, number of blocks and the network.
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup start [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for start
+```
+
+### SEE ALSO
+
+* [nodekit catchup](/reference/nodekit/catchup) - Manage Fast-Catchup for your node
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_catchup_stop.md b/docs/src/content/docs/reference/nodekit_catchup_stop.md
new file mode 100644
index 00000000..84e9b2b0
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_catchup_stop.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit catchup stop"
+slug: "reference/nodekit/catchup/stop"
+---
+## Synopsis
+
+
+Stop a fast catchup
+
+Overview:
+Stop an active Fast-Catchup. This will abort the catchup process if one has started
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup stop [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for stop
+```
+
+### SEE ALSO
+
+* [nodekit catchup](/reference/nodekit/catchup) - Manage Fast-Catchup for your node
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_configure.md b/docs/src/content/docs/reference/nodekit_configure.md
new file mode 100644
index 00000000..ddba61e0
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_configure.md
@@ -0,0 +1,26 @@
+---
+title: "nodekit configure"
+slug: "reference/nodekit/configure"
+---
+## Synopsis
+
+
+Change settings on the system (WIP)
+
+Overview:
+Tool for inspecting and updating the Algorand daemon's config.json and service files
+
+Note: This is still a work in progress. Expect bugs and rough edges.
+
+### Options
+
+```
+ -h, --help help for configure
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+* [nodekit configure service](/reference/nodekit/configure/service) - Install service files for the Algorand daemon.
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_configure_service.md b/docs/src/content/docs/reference/nodekit_configure_service.md
new file mode 100644
index 00000000..1a381651
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_configure_service.md
@@ -0,0 +1,29 @@
+---
+title: "nodekit configure service"
+slug: "reference/nodekit/configure/service"
+---
+## Synopsis
+
+
+Install service files for the Algorand daemon.
+
+Overview:
+Ensuring that the Algorand daemon is installed and running as a service.
+
+Note: This is still a work in progress. Expect bugs and rough edges.
+
+```
+nodekit configure service [flags]
+```
+
+### Options
+
+```
+ -h, --help help for service
+```
+
+### SEE ALSO
+
+* [nodekit configure](/reference/nodekit/configure) - Change settings on the system (WIP)
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_debug.md b/docs/src/content/docs/reference/nodekit_debug.md
new file mode 100644
index 00000000..74bd2352
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_debug.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit debug"
+slug: "reference/nodekit/debug"
+---
+## Synopsis
+
+
+Display debugging information
+
+Overview:
+Prints the known state of the nodekit
+Checks various paths and configurations to present useful information for bug reports.
+
+
+```
+nodekit debug [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for debug
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_install.md b/docs/src/content/docs/reference/nodekit_install.md
new file mode 100644
index 00000000..e9c36d02
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_install.md
@@ -0,0 +1,29 @@
+---
+title: "nodekit install"
+slug: "reference/nodekit/install"
+---
+## Synopsis
+
+
+Install the node daemon
+
+Overview:
+Configures the local package manager and installs the algorand daemon on your local machine
+
+
+```
+nodekit install [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully install the node
+ -h, --help help for install
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_start.md b/docs/src/content/docs/reference/nodekit_start.md
new file mode 100644
index 00000000..026e41ed
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_start.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit start"
+slug: "reference/nodekit/start"
+---
+## Synopsis
+
+
+Start the node daemon
+
+Overview:
+Start the Algorand daemon on your local machine if it is not already running. Optionally, the daemon can be forcefully started.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit start [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully start the node
+ -h, --help help for start
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_stop.md b/docs/src/content/docs/reference/nodekit_stop.md
new file mode 100644
index 00000000..6d1e87ce
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_stop.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit stop"
+slug: "reference/nodekit/stop"
+---
+## Synopsis
+
+
+Stop the node daemon
+
+Overview:
+Stops the Algorand daemon on your local machine. Optionally, the daemon can be forcefully stopped.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit stop [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully stop the node
+ -h, --help help for stop
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_uninstall.md b/docs/src/content/docs/reference/nodekit_uninstall.md
new file mode 100644
index 00000000..5a258690
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_uninstall.md
@@ -0,0 +1,30 @@
+---
+title: "nodekit uninstall"
+slug: "reference/nodekit/uninstall"
+---
+## Synopsis
+
+
+Uninstall the node daemon
+
+Overview:
+Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit uninstall [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully uninstall the node
+ -h, --help help for uninstall
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/content/docs/reference/nodekit_upgrade.md b/docs/src/content/docs/reference/nodekit_upgrade.md
new file mode 100644
index 00000000..cb60c400
--- /dev/null
+++ b/docs/src/content/docs/reference/nodekit_upgrade.md
@@ -0,0 +1,29 @@
+---
+title: "nodekit upgrade"
+slug: "reference/nodekit/upgrade"
+---
+## Synopsis
+
+
+Upgrade the node daemon
+
+Overview:
+Upgrade Algorand packages if it was installed with package manager.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit upgrade [flags]
+```
+
+### Options
+
+```
+ -h, --help help for upgrade
+```
+
+### SEE ALSO
+
+* [nodekit](/reference/nodekit) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/docs/src/pages/index.astro b/docs/src/pages/index.astro
new file mode 100644
index 00000000..00374699
--- /dev/null
+++ b/docs/src/pages/index.astro
@@ -0,0 +1,36 @@
+---
+import { Code } from '@astrojs/starlight/components';
+import { Icon } from '@astrojs/starlight/components';
+
+const base = import.meta.env.BASE_URL;
+export const lang = "en"
+---
+
+
+
+ NodeKit - Algorand Foundation
+
+
+
+
+
+
+
+
+ Follow the white rabbit
+
+
+
\ No newline at end of file
diff --git a/docs/src/tailwind.css b/docs/src/tailwind.css
new file mode 100644
index 00000000..0302ee40
--- /dev/null
+++ b/docs/src/tailwind.css
@@ -0,0 +1,37 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/*
+Add additional Tailwind styles to this file, for example with @layer:
+https://tailwindcss.com/docs/adding-custom-styles#using-css-and-layer
+*/
+/* Dark mode colors. */
+:root {
+ --sl-color-accent-low: #131e4f;
+ --sl-color-accent: #3646ff;
+ --sl-color-accent-high: #b3c7ff;
+ --sl-color-white: #ffffff;
+ --sl-color-gray-1: #e4eff9;
+ --sl-color-gray-2: #b6c4d0;
+ --sl-color-gray-3: #758fa6;
+ --sl-color-gray-4: #435b70;
+ --sl-color-gray-5: #233b4e;
+ --sl-color-gray-6: #192A39;
+ --sl-color-black: #0f1922;
+}
+/* Light mode colors. */
+:root[data-theme='light'] {
+ --sl-color-accent-low: #c7d6ff;
+ --sl-color-accent: #384bff;
+ --sl-color-accent-high: #192676;
+ --sl-color-white: #0f1922;
+ --sl-color-gray-1: #12293b;
+ --sl-color-gray-2: #233b4e;
+ --sl-color-gray-3: #435b70;
+ --sl-color-gray-4: #758fa6;
+ --sl-color-gray-5: #b6c4d0;
+ --sl-color-gray-6: #e4eff9;
+ --sl-color-gray-7: #f1f7fc;
+ --sl-color-black: #ffffff;
+}
\ No newline at end of file
diff --git a/docs/tailwind.config.mjs b/docs/tailwind.config.mjs
new file mode 100644
index 00000000..304c5d73
--- /dev/null
+++ b/docs/tailwind.config.mjs
@@ -0,0 +1,21 @@
+import colors from 'tailwindcss/colors';
+import starlightPlugin from '@astrojs/starlight-tailwind';
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
+ theme: {
+ // fontFamily: {
+ // mono: ["DejaVu Sans Mono"]
+ // },
+ extend: {
+ colors: {
+ // Your preferred accent color. Indigo is closest to Starlightโs defaults.
+ accent: colors.indigo,
+ // Your preferred gray scale. Zinc is closest to Starlightโs defaults.
+ gray: colors.zinc,
+ },
+ },
+ },
+ plugins: [starlightPlugin()],
+};
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 00000000..8bf91d3b
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/generate.yaml b/generate.yaml
index 024c5891..68a0f5da 100644
--- a/generate.yaml
+++ b/generate.yaml
@@ -20,3 +20,5 @@ output-options:
- GetBlock
- AccountInformation
- GetGenesis
+ - StartCatchup
+ - AbortCatchup
diff --git a/go.mod b/go.mod
index a04b4a7c..34d164b1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/algorandfoundation/algorun-tui
+module github.com/algorandfoundation/nodekit
go 1.22.0
@@ -8,9 +8,11 @@ require (
github.com/algorandfoundation/algourl v0.0.0-20241023193235-8bbf72ad0b37
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.1.1
+ github.com/charmbracelet/glamour v0.8.0
github.com/charmbracelet/lipgloss v0.13.1
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691
+ github.com/manifoldco/promptui v0.9.0
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1
github.com/oapi-codegen/runtime v1.1.1
github.com/spf13/cobra v1.8.1
@@ -19,8 +21,21 @@ require (
)
require (
+ github.com/alecthomas/chroma/v2 v2.14.0 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dlclark/regexp2 v1.11.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.27 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/yuin/goldmark v1.7.4 // indirect
+ github.com/yuin/goldmark-emoji v1.0.3 // indirect
+ golang.org/x/net v0.27.0 // indirect
+ golang.org/x/term v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
diff --git a/go.sum b/go.sum
index acaa1e37..16c9fd44 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,10 @@
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
+github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
+github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/algorand/go-algorand-sdk/v2 v2.6.0 h1:pfL8lloEi26l6PwAFicmPUguWgKpy1eZZTMlQcci5h0=
github.com/algorand/go-algorand-sdk/v2 v2.6.0/go.mod h1:4ayerzjoWChm3kuVhbgFgURTbaYTtlj0c41eP3av5lw=
github.com/algorand/go-codec/codec v1.1.10 h1:zmWYU1cp64jQVTOG8Tw8wa+k0VfwgXIPbnDfiVa+5QA=
@@ -15,11 +21,15 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY=
github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
+github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
+github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A=
github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
@@ -32,11 +42,20 @@ github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691 h1:xiY
github.com/charmbracelet/x/exp/teatest v0.0.0-20241022174419-46d9bb99a691/go.mod h1:ektxP4TiEONm1mTGILRfo8F0a4rZMwsT1fEkXslQKtU=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
+github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -52,8 +71,12 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
@@ -68,18 +91,25 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
+github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
@@ -91,11 +121,13 @@ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xl
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
@@ -121,6 +153,11 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
+github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
+github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -133,10 +170,13 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -147,6 +187,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/install.sh b/install.sh
index 1353da47..46aa2655 100755
--- a/install.sh
+++ b/install.sh
@@ -3,19 +3,17 @@
set -euo pipefail
-BANNER=$(cat <<'EOF'
- _____ .__ __________
- / _ \ | | ____ ____\______ \__ __ ____
- / /_\ \| | / ___\ / _ \| _/ | \/ \
-/ | \ |__/ /_/ > <_> ) | \ | / | \
-\____|__ /____/\___ / \____/|____|_ /____/|___| /
- \/ /_____/ \/ \/
-EOF
-)
+
+BANNER=' _____ .__ __________
+ / _ \ | | ____ ____\______ \__ __ ____
+ / /_\ \| | / ___\ / _ \| _/ | \/ \
+ / | \ |__/ /_/ > <_> ) | \ | / | \
+ \____|__ /____/\___ / \____/|____|_ /____/|___| /
+ \/ /_____/ \/ \/ '
os=$(uname -ms)
-release="https://github.com/algorandfoundation/algorun-tui/releases/download"
-version="v1.0.0-beta.1"
+release="https://github.com/algorandfoundation/nodekit/releases/download"
+version="v1.0.0-beta.2"
Red=''
Green=''
@@ -55,8 +53,8 @@ error() {
exit 1
}
-if [ -f algorun ]; then
- error "An algorun file already exists in the current directory. Delete or rename it before installing."
+if [ -f nodekit ]; then
+ error "An nodekit file already exists in the current directory. Delete or rename it before installing."
fi
@@ -69,28 +67,28 @@ trap "info Exiting the installation" exit
case $os in
'Darwin x86_64')
- target=algorun-amd64-darwin
+ target=nodekit-amd64-darwin
;;
'Darwin arm64')
- target=algorun-arm64-darwin
+ target=nodekit-arm64-darwin
;;
'Linux aarch64' | 'Linux arm64')
- target=algorun-arm64-linux
+ target=nodekit-arm64-linux
;;
'Linux x86_64' | *)
- target=algorun-amd64-linux
+ target=nodekit-amd64-linux
;;
esac
echo -e "${Opaque}Downloading:${Reset}${Bold_White} $target $version${Reset}"
-curl --fail --location --progress-bar --output algorun "$release/$version/$target" ||
+curl --fail --location --progress-bar --output nodekit "$release/$version/$target" ||
error "Failed to download ${target} from ${release}"
-chmod +x algorun
+chmod +x nodekit
trap - int
trap - exit
-success "Downloaded: ${Bold_Green}algorun ${version} ๐${Reset}"
+success "Downloaded: ${Bold_Green}nodekit ${version} ๐${Reset}"
info "Get started by running:"
-echo "./algorun --help"
+echo "./nodekit --help"
diff --git a/internal/accounts.go b/internal/algod/accounts.go
similarity index 53%
rename from internal/accounts.go
rename to internal/algod/accounts.go
index 28d815fe..36d30519 100644
--- a/internal/accounts.go
+++ b/internal/algod/accounts.go
@@ -1,14 +1,16 @@
-package internal
+package algod
import (
- "bytes"
"context"
"errors"
"fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/algorandfoundation/nodekit/internal/system"
"time"
"github.com/algorand/go-algorand-sdk/v2/types"
- "github.com/algorandfoundation/algorun-tui/api"
+ "github.com/algorandfoundation/nodekit/api"
)
// Account represents a user's account, including address, status, balance, and number of keys.
@@ -54,27 +56,11 @@ func GetAccount(client api.ClientWithResponsesInterface, address string) (api.Ac
return *r.JSON200, nil
}
-// GetExpiresTime calculates and returns the expiration time for a participation key based on the current account state.
-func GetExpiresTime(t Time, lastRound int, roundTime time.Duration, account Account) *time.Time {
- now := t.Now()
- var expires time.Time
- if account.Status == "Online" &&
- account.Participation != nil &&
- lastRound != 0 &&
- roundTime != 0 {
- roundDiff := max(0, account.Participation.VoteLastValid-int(lastRound))
- distance := int(roundTime) * roundDiff
- expires = now.Add(time.Duration(distance))
- return &expires
- }
- return nil
-}
-
// ParticipationKeysToAccounts converts a slice of ParticipationKey objects into a map of Account objects.
// The keys parameter is a slice of pointers to ParticipationKey instances.
// The prev parameter is an optional map that allows merging of existing accounts with new ones.
// Returns a map where each key is an address from a ParticipationKey, and the value is a corresponding Account.
-func ParticipationKeysToAccounts(keys *[]api.ParticipationKey) map[string]Account {
+func ParticipationKeysToAccounts(keys []api.ParticipationKey) map[string]Account {
// Allow merging of existing accounts
var accounts = make(map[string]Account)
@@ -84,7 +70,7 @@ func ParticipationKeysToAccounts(keys *[]api.ParticipationKey) map[string]Accoun
}
// Add missing Accounts
- for _, key := range *keys {
+ for _, key := range keys {
if _, ok := accounts[key.Address]; !ok {
accounts[key.Address] = Account{
Participation: nil,
@@ -104,10 +90,12 @@ func ParticipationKeysToAccounts(keys *[]api.ParticipationKey) map[string]Accoun
return accounts
}
-func UpdateAccountFromRPC(account Account, rpcAccount api.Account) Account {
- account.Status = rpcAccount.Status
- account.Balance = rpcAccount.Amount / 1000000
- account.Participation = rpcAccount.Participation
+// Merge updates the Account instance with data from the provided api.Account and returns the updated Account.
+// It updates fields such as Status, Balance, Participation, and IncentiveEligible based on the rpcAccount values.
+func (a Account) Merge(rpcAccount api.Account) Account {
+ a.Status = rpcAccount.Status
+ a.Balance = rpcAccount.Amount / 1000000
+ a.Participation = rpcAccount.Participation
var incentiveEligible = false
if rpcAccount.IncentiveEligible == nil {
@@ -116,57 +104,41 @@ func UpdateAccountFromRPC(account Account, rpcAccount api.Account) Account {
incentiveEligible = *rpcAccount.IncentiveEligible
}
- account.IncentiveEligible = incentiveEligible
+ a.IncentiveEligible = incentiveEligible
- return account
+ if rpcAccount.Participation != nil {
+ a.Participation = rpcAccount.Participation
+ }
+
+ return a
}
-func IsParticipationKeyActive(part api.ParticipationKey, account api.AccountParticipation) bool {
- var equal = false
- if bytes.Equal(part.Key.VoteParticipationKey, account.VoteParticipationKey) &&
- part.Key.VoteLastValid == account.VoteLastValid &&
- part.Key.VoteFirstValid == account.VoteFirstValid {
- equal = true
+// GetExpiresTime calculates the expiration time of the account's participation key based on round differences and duration.
+// Returns nil if the account has no participation or if the expiration time cannot be determined.
+func (a Account) GetExpiresTime(t system.Time, lastRound int, roundTime time.Duration) *time.Time {
+ if a.Participation == nil {
+ return nil
}
- return equal
+ return utils.GetExpiresTime(t, lastRound, roundTime, a.Participation.VoteLastValid)
}
-func UpdateAccountExpiredTime(t Time, account Account, state *StateModel) Account {
+// UpdateExpiredTime updates the account's expiration time and identifies if the account has a non-resident participation key.
+// It checks if the account is offline or if its local participation key matches one of the provided keys.
+// The method recalculates the expiration time based on the last round and round duration.
+func (a Account) UpdateExpiredTime(t system.Time, keys []api.ParticipationKey, lastRound int, roundTime time.Duration) Account {
var nonResidentKey = true
- for _, key := range *state.ParticipationKeys {
+ for _, key := range keys {
// We have the key locally, update the residency
- if account.Status == "Offline" || (key.Address == account.Address && account.Participation != nil && IsParticipationKeyActive(key, *account.Participation)) {
+ if a.Status == "Offline" || (key.Address == a.Address && a.Participation != nil && participation.IsActive(key, *a.Participation)) {
nonResidentKey = false
}
}
- account.NonResidentKey = nonResidentKey
- account.Expires = GetExpiresTime(t, int(state.Status.LastRound), state.Metrics.RoundTime, account)
- return account
-}
-
-// AccountsFromState maps an array of api.ParticipationKey to a keyed map of Account
-func AccountsFromState(state *StateModel, t Time, client api.ClientWithResponsesInterface) (map[string]Account, error) {
- if state == nil {
- return make(map[string]Account), nil
- }
-
- accounts := ParticipationKeysToAccounts(state.ParticipationKeys)
-
- for _, acct := range accounts {
- // For each account, update the data from the RPC endpoint
- if state.Status.State != SyncingState {
- rpcAcct, err := GetAccount(client, acct.Address)
- if err != nil {
- return nil, err
- }
- accounts[acct.Address] = UpdateAccountFromRPC(acct, rpcAcct)
- accounts[acct.Address] = UpdateAccountExpiredTime(t, accounts[acct.Address], state)
- }
- }
-
- return accounts, nil
+ a.NonResidentKey = nonResidentKey
+ a.Expires = a.GetExpiresTime(t, lastRound, roundTime)
+ return a
}
+// ValidateAddress checks the validity of an Algorand address by decoding it. Returns true for valid addresses, false otherwise.
func ValidateAddress(address string) bool {
_, err := types.DecodeAddress(address)
if err != nil {
diff --git a/internal/accounts_test.go b/internal/algod/accounts_test.go
similarity index 65%
rename from internal/accounts_test.go
rename to internal/algod/accounts_test.go
index e1f23e82..8da725c7 100644
--- a/internal/accounts_test.go
+++ b/internal/algod/accounts_test.go
@@ -1,10 +1,10 @@
-package internal
+package algod
import (
"context"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal/test"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
"github.com/stretchr/testify/assert"
"testing"
@@ -12,7 +12,7 @@ import (
)
func Test_AccountsFromState(t *testing.T) {
-
+ t.Skip()
// Setup elevated client
apiToken, err := securityprovider.NewSecurityProviderApiKey("header", "X-Algo-API-Token", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
if err != nil {
@@ -64,7 +64,7 @@ func Test_AccountsFromState(t *testing.T) {
// Mock StateModel
state := &StateModel{
- Metrics: MetricsModel{
+ Metrics: Metrics{
Enabled: true,
Window: 100,
RoundTime: time.Duration(2) * time.Second,
@@ -72,44 +72,23 @@ func Test_AccountsFromState(t *testing.T) {
RX: 1024,
TX: 2048,
},
- Status: StatusModel{
+ Status: Status{
State: "WATCHING",
Version: "v0.0.0-test",
Network: "tuinet",
Voting: false,
NeedsUpdate: false,
LastRound: 1337,
+ Client: client,
+ HttpPkg: new(api.HttpPkg),
},
- ParticipationKeys: &mock.Keys,
+ ParticipationKeys: mock.Keys,
+ Client: client,
+ HttpPkg: new(api.HttpPkg),
}
// Calculate expiration
clock := new(mock.Clock)
- now := clock.Now()
- roundDiff := max(0, mock.Keys[0].Key.VoteLastValid-int(state.Status.LastRound))
- distance := int(state.Metrics.RoundTime) * roundDiff
- expires := now.Add(time.Duration(distance))
- tClient := test.GetClient(false)
- acct, _ = GetAccount(tClient, "ABC")
- // Construct expected accounts
- expectedAccounts := map[string]Account{
- "ABC": {
- Participation: acct.Participation,
- Address: acct.Address,
- Status: acct.Status,
- IncentiveEligible: true,
- Balance: acct.Amount / 1_000_000,
- Keys: 2,
- Expires: &expires,
- },
- }
-
- // Call AccountsFromState
- accounts, err := AccountsFromState(state, clock, tClient)
- if err != nil {
- t.Fatal(err)
- }
- // Assert results
- assert.Equal(t, expectedAccounts, accounts)
+ state.UpdateKeys(context.Background(), clock)
}
diff --git a/internal/algod/algod.go b/internal/algod/algod.go
new file mode 100644
index 00000000..aed0ad5a
--- /dev/null
+++ b/internal/algod/algod.go
@@ -0,0 +1,146 @@
+package algod
+
+import (
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod/linux"
+ "github.com/algorandfoundation/nodekit/internal/algod/mac"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "runtime"
+)
+
+// UnsupportedOSError indicates that the current operating system is not supported for the requested operation.
+const UnsupportedOSError = "unsupported operating system"
+
+// InvalidStatusResponseError represents an error message indicating an invalid response status was encountered.
+const InvalidStatusResponseError = "invalid status response"
+
+// InvalidVersionResponseError represents an error message for an invalid response from the version endpoint.
+const InvalidVersionResponseError = "invalid version response"
+
+// IsInstalled checks if the Algod software is installed on the system
+// by verifying its presence and service setup.
+func IsInstalled() bool {
+ return system.CmdExists("algod")
+}
+
+// IsRunning checks if the algod is currently running on the host operating system.
+// It returns true if the application is running, or false if it is not or if an error occurs.
+// This function supports Linux and macOS platforms. It returns an error for unsupported operating systems.
+func IsRunning() bool {
+ switch runtime.GOOS {
+ case "linux", "darwin":
+ return system.IsCmdRunning("algod")
+
+ default:
+ return false
+ }
+}
+
+// IsService determines if the Algorand service is configured as
+// a system service on the current operating system.
+func IsService() bool {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.IsService()
+ case "darwin":
+ return mac.IsService()
+ default:
+ return false
+ }
+}
+
+// SetNetwork configures the network to the specified setting
+// or returns an error on unsupported operating systems.
+func SetNetwork(network string) error {
+ return fmt.Errorf(UnsupportedOSError)
+}
+
+// Install installs Algorand software based on the host OS
+// and returns an error if the installation fails or is unsupported.
+func Install() error {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.Install()
+ case "darwin":
+ return mac.Install()
+ default:
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
+
+// Update checks the operating system and performs an
+// upgrade using OS-specific package managers, if supported.
+func Update() error {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.Upgrade()
+ case "darwin":
+ return mac.Upgrade(false)
+ default:
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
+
+// Uninstall removes the Algorand software from the system based
+// on the host operating system using appropriate methods.
+func Uninstall(force bool) error {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.Uninstall()
+ case "darwin":
+ return mac.Uninstall(force)
+ default:
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
+
+// UpdateService updates the service configuration for the
+// Algorand daemon based on the OS and reloads the service.
+func UpdateService(dataDirectoryPath string) error {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.UpdateService(dataDirectoryPath)
+ case "darwin":
+ return mac.UpdateService(dataDirectoryPath)
+ default:
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
+
+// EnsureService ensures the `algod` service is configured and running
+// as a service based on the OS;
+// Returns an error for unsupported systems.
+func EnsureService() error {
+ switch runtime.GOOS {
+ case "darwin":
+ return mac.EnsureService()
+ default:
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
+
+// Start attempts to initiate the Algod service based on the
+// host operating system. Returns an error for unsupported OS.
+func Start() error {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.Start()
+ case "darwin":
+ return mac.Start(false)
+ default: // Unsupported OS
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
+
+// Stop shuts down the Algorand algod system process based on the current operating system.
+// Returns an error if the operation fails or the operating system is unsupported.
+func Stop() error {
+ switch runtime.GOOS {
+ case "linux":
+ return linux.Stop()
+ case "darwin":
+ return mac.Stop(false)
+ default:
+ return fmt.Errorf(UnsupportedOSError)
+ }
+}
diff --git a/internal/algod/block.go b/internal/algod/block.go
new file mode 100644
index 00000000..5829ee66
--- /dev/null
+++ b/internal/algod/block.go
@@ -0,0 +1,65 @@
+package algod
+
+import (
+ "context"
+ "errors"
+ "github.com/algorandfoundation/nodekit/api"
+ "time"
+)
+
+// BlockMetrics represents metrics of a blockchain segment, including average block time and transactions per second.
+type BlockMetrics struct {
+ AvgTime time.Duration
+ TPS float64
+}
+
+// GetBlockMetrics calculates block metrics such as average block time and transactions per second within a specified window.
+func GetBlockMetrics(ctx context.Context, client api.ClientWithResponsesInterface, round uint64, window int) (BlockMetrics, api.ResponseInterface, error) {
+ var avgs = BlockMetrics{
+ AvgTime: 0,
+ TPS: 0,
+ }
+ var format api.GetBlockParamsFormat = "json"
+
+ // Current Block
+ currentBlockResponse, err := client.GetBlockWithResponse(ctx, int(round), &api.GetBlockParams{
+ Format: &format,
+ })
+ if err != nil {
+ return avgs, currentBlockResponse, err
+ }
+ if currentBlockResponse.StatusCode() != 200 {
+ return avgs, currentBlockResponse, errors.New(currentBlockResponse.Status())
+ }
+
+ // Previous Block Response
+ previousBlockResponse, err := client.GetBlockWithResponse(ctx, int(round)-window, &api.GetBlockParams{
+ Format: &format,
+ })
+ if err != nil {
+ return avgs, previousBlockResponse, err
+ }
+ if previousBlockResponse.StatusCode() != 200 {
+ return avgs, previousBlockResponse, errors.New(previousBlockResponse.Status())
+ }
+
+ // Push to the transactions count list
+ aTimestampRes := currentBlockResponse.JSON200.Block["ts"]
+ bTimestampRes := previousBlockResponse.JSON200.Block["ts"]
+ if aTimestampRes == nil || bTimestampRes == nil {
+ return avgs, previousBlockResponse, nil
+ }
+ aTimestamp := time.Duration(aTimestampRes.(float64)) * time.Second
+ bTimestamp := time.Duration(bTimestampRes.(float64)) * time.Second
+
+ // Transaction Counter
+ aTransactions := currentBlockResponse.JSON200.Block["tc"]
+ bTransactions := previousBlockResponse.JSON200.Block["tc"]
+
+ avgs.AvgTime = time.Duration((int(aTimestamp - bTimestamp)) / window)
+ if aTransactions != nil && bTransactions != nil {
+ avgs.TPS = (aTransactions.(float64) - bTransactions.(float64)) / (float64(window) * avgs.AvgTime.Seconds())
+ }
+
+ return avgs, currentBlockResponse, nil
+}
diff --git a/internal/block_test.go b/internal/algod/block_test.go
similarity index 77%
rename from internal/block_test.go
rename to internal/algod/block_test.go
index 5b39feed..5d33fa69 100644
--- a/internal/block_test.go
+++ b/internal/algod/block_test.go
@@ -1,8 +1,8 @@
-package internal
+package algod
import (
"context"
- "github.com/algorandfoundation/algorun-tui/api"
+ "github.com/algorandfoundation/nodekit/api"
"testing"
"time"
)
@@ -14,7 +14,7 @@ func Test_GetBlockMetrics(t *testing.T) {
client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
- metrics, err := GetBlockMetrics(context.Background(), client, uint64(42000000), window)
+ metrics, _, err := GetBlockMetrics(context.Background(), client, uint64(42000000), window)
if err != nil {
t.Fatal(err)
}
diff --git a/internal/algod/catchpoint.go b/internal/algod/catchpoint.go
new file mode 100644
index 00000000..85540431
--- /dev/null
+++ b/internal/algod/catchpoint.go
@@ -0,0 +1,47 @@
+package algod
+
+import (
+ "context"
+ "errors"
+ "github.com/algorandfoundation/nodekit/api"
+)
+
+// StartCatchup sends a request to start a catchup operation on a specific catchpoint and returns the catchup message.
+// It uses the provided API client, catchpoint string, and optional parameters for catchup configuration.
+// Returns the catchup message, the raw API response, and an error if any occurred.
+func StartCatchup(ctx context.Context, client api.ClientWithResponsesInterface, catchpoint string, params *api.StartCatchupParams) (string, api.ResponseInterface, error) {
+ response, err := client.StartCatchupWithResponse(ctx, catchpoint, params)
+ if err != nil {
+ return "", response, err
+ }
+ if response.StatusCode() >= 300 {
+ return "", response, errors.New(response.Status())
+ }
+ if response.StatusCode() == 200 {
+ return response.JSON200.CatchupMessage, response, nil
+ }
+
+ return response.JSON201.CatchupMessage, response, nil
+}
+
+// AbortCatchup aborts a ledger catchup process for the specified catchpoint using the provided client interface.
+func AbortCatchup(ctx context.Context, client api.ClientWithResponsesInterface, catchpoint string) (string, api.ResponseInterface, error) {
+ response, err := client.AbortCatchupWithResponse(ctx, catchpoint)
+ if err != nil {
+ return "", response, err
+ }
+ if response.StatusCode() >= 300 {
+ return "", response, errors.New(response.Status())
+ }
+
+ return response.JSON200.CatchupMessage, response, nil
+}
+
+// GetLatestCatchpoint fetches the latest catchpoint for the specified network using the provided HTTP package.
+func GetLatestCatchpoint(httpPkg api.HttpPkgInterface, network string) (string, api.ResponseInterface, error) {
+ response, err := api.GetLatestCatchpointWithResponse(httpPkg, network)
+ if err != nil {
+ return "", response, err
+ }
+ return response.JSON200, response, nil
+}
diff --git a/internal/algod/client.go b/internal/algod/client.go
new file mode 100644
index 00000000..aea44b78
--- /dev/null
+++ b/internal/algod/client.go
@@ -0,0 +1,59 @@
+package algod
+
+import (
+ "errors"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+const InvalidDataDirMsg = "invalid data directory"
+
+func GetDataDir(dataDir string) (string, error) {
+ envDataDir := os.Getenv("ALGORAND_DATA")
+
+ var defaultDataDir string
+ switch runtime.GOOS {
+ case "darwin":
+ defaultDataDir = filepath.Join(os.Getenv("HOME"), ".algorand")
+ case "linux":
+ defaultDataDir = "/var/lib/algorand"
+ default:
+ return "", errors.New(UnsupportedOSError)
+ }
+
+ var resolvedDir string
+
+ if dataDir == "" {
+ if envDataDir == "" {
+ resolvedDir = defaultDataDir
+ } else {
+ resolvedDir = envDataDir
+ }
+ } else {
+ resolvedDir = dataDir
+ }
+
+ return resolvedDir, nil
+}
+
+// GetClient initializes and returns a new API client configured with the provided endpoint and access token.
+func GetClient(dataDir string) (*api.ClientWithResponses, error) {
+ resolvedDir, err := GetDataDir(dataDir)
+ if err != nil {
+ return nil, err
+ }
+ config, err := utils.ToDataFolderConfig(resolvedDir)
+ if err != nil {
+ return nil, err
+ }
+
+ apiToken, err := securityprovider.NewSecurityProviderApiKey("header", "X-Algo-API-Token", config.Token)
+ if err != nil {
+ return nil, err
+ }
+ return api.NewClientWithResponses(config.Endpoint, api.WithRequestEditorFn(apiToken.Intercept))
+}
diff --git a/internal/algod/fallback/algod.go b/internal/algod/fallback/algod.go
new file mode 100644
index 00000000..7cb86adc
--- /dev/null
+++ b/internal/algod/fallback/algod.go
@@ -0,0 +1,94 @@
+package fallback
+
+import (
+ "errors"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod/msgs"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/charmbracelet/log"
+ "os"
+ "os/exec"
+ "syscall"
+)
+
+// Install executes a series of commands to set up the Algorand node and development tools on a Unix environment.
+// TODO: Allow for changing of the paths
+func Install() error {
+ return system.RunAll(system.CmdsList{
+ {"mkdir", "~/node"},
+ {"sh", "-c", "cd ~/node"},
+ {"wget", "https://raw.githubusercontent.com/algorand/go-algorand/rel/stable/cmd/updater/update.sh"},
+ {"chmod", "744", "update.sh"},
+ {"sh", "-c", "./update.sh -i -c stable -p ~/node -d ~/node/data -n"},
+ })
+
+}
+
+// Start initializes and starts the `algod` process and verifies if the ALGORAND_DATA environment variable is valid.
+func Start() error {
+ path, err := exec.LookPath("algod")
+ log.Debug("Starting algod", "path", path)
+
+ // Check if ALGORAND_DATA environment variable is set
+ log.Info("Checking if ALGORAND_DATA env var is set...")
+ algorandData := os.Getenv("ALGORAND_DATA")
+
+ if !utils.IsDataDir(algorandData) {
+ return errors.New(msgs.InvalidDataDirectory)
+ }
+
+ log.Info("ALGORAND_DATA env var set to valid directory: " + algorandData)
+
+ cmd := exec.Command("algod")
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Setsid: true,
+ }
+ err = cmd.Start()
+ if err != nil {
+ return fmt.Errorf("Failed to start algod: %v\n", err)
+ }
+ return nil
+}
+
+// Stop gracefully shuts down the algod process by sending a SIGTERM signal to its process ID. It returns an error if any occurs.
+func Stop() error {
+ log.Debug("Manually shutting down algod")
+ // Find the process ID of algod
+ pid, err := findAlgodPID()
+ if err != nil {
+ return err
+ }
+
+ // Send SIGTERM to the process
+ process, err := os.FindProcess(pid)
+ if err != nil {
+ return err
+ }
+
+ err = process.Signal(syscall.SIGTERM)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// findAlgodPID locates the process ID of the running algod instance by executing the "pgrep" command.
+// It returns the process ID as an integer or an error if the process is not found or the command execution fails.
+func findAlgodPID() (int, error) {
+ log.Debug("Scanning for algod process")
+ cmd := exec.Command("pgrep", "algod")
+ output, err := cmd.Output()
+ if err != nil {
+ return 0, err
+ }
+
+ var pid int
+ _, err = fmt.Sscanf(string(output), "%d", &pid)
+ if err != nil {
+ return 0, err
+ }
+
+ return pid, nil
+}
diff --git a/internal/algod/linux/linux.go b/internal/algod/linux/linux.go
new file mode 100644
index 00000000..ec96c154
--- /dev/null
+++ b/internal/algod/linux/linux.go
@@ -0,0 +1,223 @@
+package linux
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod/fallback"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/charmbracelet/log"
+ "os"
+ "os/exec"
+ "runtime"
+ "strings"
+ "text/template"
+)
+
+// PackageManagerNotFoundMsg is an error message indicating the absence of a supported package manager for uninstalling Algorand.
+const PackageManagerNotFoundMsg = "could not find a package manager to uninstall Algorand"
+
+// Algod represents an implementation of the system.Interface tailored for managing the Algod service.
+// It includes details about the service's executable path and associated data directory.
+type Algod struct {
+ system.Interface
+ Path string
+ DataDirectoryPath string
+}
+
+// InstallRequirements generates installation commands for "sudo" based on the detected package manager and system state.
+func InstallRequirements() system.CmdsList {
+ var cmds system.CmdsList
+ if (system.CmdExists("sudo") && system.CmdExists("prep")) || os.Geteuid() != 0 {
+ return cmds
+ }
+ if system.CmdExists("apt-get") {
+ return system.CmdsList{
+ {"apt-get", "update"},
+ {"apt-get", "install", "-y", "sudo", "procps"},
+ }
+ }
+
+ if system.CmdExists("dnf") {
+ return system.CmdsList{
+ {"dnf", "install", "-y", "sudo", "procps-ng"},
+ }
+ }
+ return cmds
+}
+
+// Install installs Algorand development tools or node software depending on the package manager.
+func Install() error {
+ log.Info("Installing Algod on Linux")
+ // Based off of https://developer.algorand.org/docs/run-a-node/setup/install/#installation-with-a-package-manager
+ if system.CmdExists("apt-get") { // On some Debian systems we use apt-get
+ log.Info("Installing with apt-get")
+ return system.RunAll(append(InstallRequirements(), system.CmdsList{
+ {"sudo", "apt-get", "update"},
+ {"sudo", "apt-get", "install", "-y", "gnupg2", "curl", "software-properties-common"},
+ {"sh", "-c", "curl -o - https://releases.algorand.com/key.pub | sudo tee /etc/apt/trusted.gpg.d/algorand.asc"},
+ {"sudo", "add-apt-repository", "-y", fmt.Sprintf("deb [arch=%s] https://releases.algorand.com/deb/ stable main", runtime.GOARCH)},
+ {"sudo", "apt-get", "update"},
+ {"sudo", "apt-get", "install", "-y", "algorand"},
+ }...))
+ }
+
+ if system.CmdExists("dnf") { // On Fedora and CentOs8 there's the dnf package manager
+ log.Printf("Installing with dnf")
+ return system.RunAll(append(InstallRequirements(), system.CmdsList{
+ {"curl", "-O", "https://releases.algorand.com/rpm/rpm_algorand.pub"},
+ {"sudo", "rpmkeys", "--import", "rpm_algorand.pub"},
+ {"sudo", "dnf", "install", "-y", "dnf-command(config-manager)"},
+ {"sudo", "dnf", "config-manager", "--add-repo=https://releases.algorand.com/rpm/stable/algorand.repo"},
+ {"sudo", "dnf", "install", "-y", "algorand"},
+ {"sudo", "systemctl", "enable", "algorand.service"},
+ {"sudo", "systemctl", "start", "algorand.service"},
+ {"rm", "-f", "rpm_algorand.pub"},
+ }...))
+
+ }
+
+ // TODO: watch this method to see if it is ever used
+ return fallback.Install()
+}
+
+// Uninstall removes the Algorand software using a supported package manager or clears related system files if necessary.
+// Returns an error if a supported package manager is not found or if any command fails during execution.
+func Uninstall() error {
+ log.Info("Uninstalling Algorand")
+ var unInstallCmds system.CmdsList
+ // On Ubuntu and Debian there's the apt package manager
+ if system.CmdExists("apt-get") {
+ log.Info("Using apt-get package manager")
+ unInstallCmds = [][]string{
+ {"sudo", "apt-get", "autoremove", "algorand", "-y"},
+ }
+ }
+ // On Fedora and CentOs8 there's the dnf package manager
+ if system.CmdExists("dnf") {
+ log.Info("Using dnf package manager")
+ unInstallCmds = [][]string{
+ {"sudo", "dnf", "remove", "algorand", "-y"},
+ }
+ }
+ // Error on unsupported package managers
+ if len(unInstallCmds) == 0 {
+ return fmt.Errorf(PackageManagerNotFoundMsg)
+ }
+
+ // Commands to clear systemd algorand.service and any other files, like the configuration override
+ unInstallCmds = append(unInstallCmds, []string{"sudo", "bash", "-c", "rm -rf /etc/systemd/system/algorand*"})
+ unInstallCmds = append(unInstallCmds, []string{"sudo", "systemctl", "daemon-reload"})
+
+ return system.RunAll(unInstallCmds)
+}
+
+// Upgrade updates Algorand and its dev tools using an approved package
+// manager if available, otherwise returns an error.
+func Upgrade() error {
+ if system.CmdExists("apt-get") {
+ return system.RunAll(system.CmdsList{
+ {"sudo", "apt-get", "update"},
+ {"sudo", "apt-get", "install", "--only-upgrade", "-y", "algorand"},
+ })
+ }
+ if system.CmdExists("dnf") {
+ return system.RunAll(system.CmdsList{
+ {"sudo", "dnf", "update", "-y", "--refresh", "algorand"},
+ })
+ }
+ return fmt.Errorf("the *node upgrade* command is currently only available for installations done with an approved package manager. Please use a different method to upgrade")
+}
+
+// Start attempts to start the Algorand service using the system's service manager.
+// It executes the appropriate command for systemd on Linux-based systems.
+// Returns an error if the command fails.
+// TODO: Replace with D-Bus integration
+func Start() error {
+ return exec.Command("sudo", "systemctl", "start", "algorand").Run()
+}
+
+// Stop shuts down the Algorand algod system process on Linux using the systemctl stop command.
+// Returns an error if the operation fails.
+// TODO: Replace with D-Bus integration
+func Stop() error {
+ return exec.Command("sudo", "systemctl", "stop", "algorand").Run()
+}
+
+// IsService checks if the "algorand.service" is listed as a systemd unit file on Linux.
+// Returns true if it exists.
+// TODO: Replace with D-Bus integration
+func IsService() bool {
+ out, err := system.Run([]string{"sudo", "systemctl", "list-unit-files", "algorand.service"})
+ if err != nil {
+ return false
+ }
+ return strings.Contains(out, "algorand.service")
+}
+
+// UpdateService updates the systemd service file for the Algorand daemon
+// with a new data directory path and reloads the daemon.
+func UpdateService(dataDirectoryPath string) error {
+
+ algodPath, err := exec.LookPath("algod")
+ if err != nil {
+ fmt.Printf("Failed to find algod binary: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Path to the systemd service override file
+ // Assuming that this is the same everywhere systemd is used
+ overrideFilePath := "/etc/systemd/system/algorand.service.d/override.conf"
+
+ // Create the override directory if it doesn't exist
+ err = os.MkdirAll("/etc/systemd/system/algorand.service.d", 0755)
+ if err != nil {
+ fmt.Printf("Failed to create override directory: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Content of the override file
+ const overrideTemplate = `[Unit]
+Description=Algorand daemon {{.AlgodPath}} in {{.DataDirectoryPath}}
+[Service]
+ExecStart=
+ExecStart={{.AlgodPath}} -d {{.DataDirectoryPath}}`
+
+ // Data to fill the template
+ data := map[string]string{
+ "AlgodPath": algodPath,
+ "DataDirectoryPath": dataDirectoryPath,
+ }
+
+ // Parse and execute the template
+ tmpl, err := template.New("override").Parse(overrideTemplate)
+ if err != nil {
+ fmt.Printf("Failed to parse template: %v\n", err)
+ os.Exit(1)
+ }
+
+ var overrideContent bytes.Buffer
+ err = tmpl.Execute(&overrideContent, data)
+ if err != nil {
+ fmt.Printf("Failed to execute template: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Write the override content to the file
+ err = os.WriteFile(overrideFilePath, overrideContent.Bytes(), 0644)
+ if err != nil {
+ fmt.Printf("Failed to write override file: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Reload systemd manager configuration
+ cmd := exec.Command("systemctl", "daemon-reload")
+ err = cmd.Run()
+ if err != nil {
+ fmt.Printf("Failed to reload systemd daemon: %v\n", err)
+ os.Exit(1)
+ }
+
+ log.Info("Algorand service file updated successfully.")
+
+ return nil
+}
diff --git a/internal/algod/mac/mac.go b/internal/algod/mac/mac.go
new file mode 100644
index 00000000..3286ca15
--- /dev/null
+++ b/internal/algod/mac/mac.go
@@ -0,0 +1,323 @@
+package mac
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod/utils"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "text/template"
+)
+
+// MustBeServiceMsg is an error message indicating that a service must be installed to manage it.
+const MustBeServiceMsg = "service must be installed to be able to manage it"
+
+// HomeBrewNotFoundMsg is the error message returned when Homebrew is not detected on the system during execution.
+const HomeBrewNotFoundMsg = "homebrew is not installed. please install Homebrew and try again"
+
+// IsService check if Algorand service has been created with launchd (macOS)
+// Note that it needs to be run in super-user privilege mode to
+// be able to view the root level services.
+func IsService() bool {
+ _, err := system.Run([]string{"sudo", "launchctl", "list", "com.algorand.algod"})
+ return err == nil
+}
+
+// Install sets up Algod on macOS using Homebrew,
+// configures necessary directories, and ensures it
+// runs as a background service.
+func Install() error {
+ log.Info("Installing Algod on macOS...")
+
+ // Homebrew is our package manager of choice
+ if !system.CmdExists("brew") {
+ return errors.New(HomeBrewNotFoundMsg)
+ }
+
+ err := system.RunAll(system.CmdsList{
+ {"brew", "tap", "algorandfoundation/homebrew-node"},
+ {"brew", "install", "algorand"},
+ {"brew", "--prefix", "algorand", "--installed"},
+ })
+ if err != nil {
+ return err
+ }
+
+ // Handle data directory and genesis.json file
+ err = handleDataDirMac()
+ if err != nil {
+ return err
+ }
+
+ path, err := os.Executable()
+ if err != nil {
+ return err
+ }
+
+ // Create and load the launchd service
+ // TODO: find a clever way to avoid this or make sudo persist for the second call
+ err = system.RunAll(system.CmdsList{{"sudo", path, "configure", "service"}})
+ if err != nil {
+ return err
+ }
+
+ if !IsService() {
+ return fmt.Errorf("algod is not a service")
+ }
+
+ log.Info("Installed Algorand (Algod) with Homebrew ")
+
+ return nil
+}
+
+// Uninstall removes the Algorand application from the system using Homebrew if it is installed.
+func Uninstall(force bool) error {
+ if force {
+ if system.IsCmdRunning("algod") {
+ err := Stop(force)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ cmds := system.CmdsList{}
+ if IsService() {
+ cmds = append(cmds, []string{"sudo", "launchctl", "unload", "/Library/LaunchDaemons/com.algorand.algod.plist"})
+ }
+
+ if !system.CmdExists("brew") && !force {
+ return errors.New("homebrew is not installed")
+ } else {
+ cmds = append(cmds, []string{"brew", "uninstall", "algorand"})
+ }
+
+ if force {
+ cmds = append(cmds, []string{"sudo", "rm", "-rf", strings.Join(utils.GetKnownDataPaths(), " ")})
+ cmds = append(cmds, []string{"sudo", "rm", "-rf", "/Library/LaunchDaemons/com.algorand.algod.plist"})
+ }
+
+ return system.RunAll(cmds)
+}
+
+// Upgrade updates the installed Algorand package using Homebrew if it's available and properly configured.
+func Upgrade(force bool) error {
+ if !system.CmdExists("brew") {
+ return errors.New("homebrew is not installed")
+ }
+
+ return system.RunAll(system.CmdsList{
+ {"brew", "--prefix", "algorand", "--installed"},
+ {"brew", "upgrade", "algorand", "--formula"},
+ })
+}
+
+// Start algorand with launchd
+func Start(force bool) error {
+ log.Debug("Attempting to start algorand with launchd")
+ //if !IsService() && !force {
+ // return fmt.Errorf(MustBeServiceMsg)
+ //}
+ return system.RunAll(system.CmdsList{
+ {"sudo", "launchctl", "start", "com.algorand.algod"},
+ })
+}
+
+// Stop shuts down the Algorand algod system process using the launchctl bootout command.
+// Returns an error if the operation fails.
+func Stop(force bool) error {
+ if !IsService() && !force {
+ return fmt.Errorf(MustBeServiceMsg)
+ }
+
+ return system.RunAll(system.CmdsList{
+ {"sudo", "launchctl", "stop", "com.algorand.algod"},
+ })
+}
+
+// UpdateService updates the Algorand launchd service with
+// a new data directory path and reloads the service configuration.
+// TODO: Deduplicate this method, redundant version of EnsureService.
+func UpdateService(dataDirectoryPath string) error {
+
+ algodPath, err := exec.LookPath("algod")
+ if err != nil {
+ log.Info("Failed to find algod binary: %v\n", err)
+ os.Exit(1)
+ }
+
+ overwriteFilePath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+
+ overwriteTemplate := `
+
+
+
+ Label
+ com.algorand.algod
+ ProgramArguments
+
+ {{.AlgodPath}}
+ -d
+ {{.DataDirectoryPath}}
+
+ RunAtLoad
+
+ StandardOutPath
+ /tmp/algod.out
+ StandardErrorPath
+ /tmp/algod.err
+
+ `
+
+ // Data to fill the template
+ data := map[string]string{
+ "AlgodPath": algodPath,
+ "DataDirectoryPath": dataDirectoryPath,
+ }
+
+ // Parse and execute the template
+ tmpl, err := template.New("override").Parse(overwriteTemplate)
+ if err != nil {
+ log.Info("Failed to parse template: %v\n", err)
+ os.Exit(1)
+ }
+
+ var overwriteContent bytes.Buffer
+ err = tmpl.Execute(&overwriteContent, data)
+ if err != nil {
+ log.Info("Failed to execute template: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Write the override content to the file
+ err = os.WriteFile(overwriteFilePath, overwriteContent.Bytes(), 0644)
+ if err != nil {
+ log.Info("Failed to write override file: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Boot out the launchd service (just in case - it should be off)
+ cmd := exec.Command("launchctl", "bootout", "system", overwriteFilePath)
+ err = cmd.Run()
+ if err != nil {
+ if !strings.Contains(err.Error(), "No such process") {
+ log.Info("Failed to bootout launchd service: %v\n", err)
+ os.Exit(1)
+ }
+ }
+
+ // Load the launchd service
+ cmd = exec.Command("launchctl", "load", overwriteFilePath)
+ err = cmd.Run()
+ if err != nil {
+ log.Info("Failed to load launchd service: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Println("Launchd service updated and reloaded successfully.")
+ return nil
+}
+
+// handleDataDirMac ensures the necessary Algorand data directory and mainnet genesis.json file exist on macOS.
+// TODO move to configure as a generic
+func handleDataDirMac() error {
+ // Ensure the ~/.algorand directory exists
+ algorandDir := filepath.Join(os.Getenv("HOME"), ".algorand")
+ if err := os.MkdirAll(algorandDir, 0755); err != nil {
+ return err
+ }
+
+ // Check if genesis.json file exists in ~/.algorand
+ // TODO: replace with algocfg or goal templates
+ genesisFilePath := filepath.Join(os.Getenv("HOME"), ".algorand", "genesis.json")
+ _, err := os.Stat(genesisFilePath)
+ if !os.IsNotExist(err) {
+ return nil
+ }
+
+ log.Info("Downloading mainnet genesis.json file to ~/.algorand/genesis.json")
+
+ // Download the genesis.json file
+ resp, err := http.Get("https://raw.githubusercontent.com/algorand/go-algorand/db7f1627e4919b05aef5392504e48b93a90a0146/installer/genesis/mainnet/genesis.json")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ // Create the file
+ out, err := os.Create(genesisFilePath)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ // Write the content to the file
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ return err
+ }
+
+ log.Info("mainnet genesis.json file downloaded successfully.")
+ return nil
+}
+
+// EnsureService ensures the `algod` service is properly configured and running as a background service on macOS.
+// It checks for the existence of the `algod` binary, creates a launchd plist file, and loads it using `launchctl`.
+// Returns an error if the binary is not found, or if any system command fails.
+func EnsureService() error {
+ log.Debug("Ensuring Algorand service is running")
+ path, err := exec.LookPath("algod")
+ if err != nil {
+ log.Error("algod does not exist in path")
+ return err
+ }
+ // Define the launchd plist content
+ plistContent := fmt.Sprintf(`
+
+
+
+ Label
+ com.algorand.algod
+ ProgramArguments
+
+ %s
+ -d
+ %s/.algorand
+
+ RunAtLoad
+
+ Debug
+
+ StandardOutPath
+ /tmp/algod.out
+ StandardErrorPath
+ /tmp/algod.err
+
+`, path, os.Getenv("HOME"))
+
+ // Write the plist content to a file
+ plistPath := "/Library/LaunchDaemons/com.algorand.algod.plist"
+ err = os.MkdirAll(filepath.Dir(plistPath), 0755)
+ if err != nil {
+ log.Info("Failed to create LaunchDaemons directory: %v\n", err)
+ cobra.CheckErr(err)
+ }
+
+ err = os.WriteFile(plistPath, []byte(plistContent), 0644)
+ if err != nil {
+ log.Info("Failed to write plist file: %v\n", err)
+ cobra.CheckErr(err)
+ }
+ return system.RunAll(system.CmdsList{
+ {"launchctl", "load", plistPath},
+ {"launchctl", "list", "com.algorand.algod"},
+ })
+}
diff --git a/internal/algod/metrics.go b/internal/algod/metrics.go
new file mode 100644
index 00000000..7a01fcac
--- /dev/null
+++ b/internal/algod/metrics.go
@@ -0,0 +1,165 @@
+package algod
+
+import (
+ "context"
+ "errors"
+ "github.com/algorandfoundation/nodekit/api"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Metrics represents runtime and performance metrics,
+// including network traffic stats, TPS, and round time data.
+type Metrics struct {
+
+ // Enabled indicates whether metrics collection and processing are active.
+ // If false, metrics are disabled or unavailable.
+ Enabled bool
+
+ // Window defines the range of rounds to consider when calculating metrics
+ // such as TPS and average RoundTime.
+ Window int
+
+ // RoundTime represents the average duration of a round,
+ // calculated based on recent round metrics.
+ RoundTime time.Duration
+
+ // TPS represents the calculated transactions per second,
+ // based on the recent metrics over a defined window of rounds.
+ TPS float64
+
+ // RX represents the number of bytes received per second,
+ // calculated from network metrics over a time interval.
+ RX int
+
+ // TX represents the number of bytes sent per second,
+ // calculated from network metrics over a defined time interval.
+ TX int
+
+ // LastTS represents the timestamp of the last update to metrics,
+ // used for calculating time deltas and rate metrics.
+ LastTS time.Time
+
+ // LastRX stores the total number of bytes received since the
+ // last metrics update, used for RX rate calculation.
+ LastRX int
+
+ // LastTX stores the total number of bytes sent since the
+ // last metrics update, used for TX rate calculation.
+ LastTX int
+
+ // Client provides an interface for interacting with API endpoints,
+ // enabling metrics retrieval and other operations.
+ Client api.ClientWithResponsesInterface
+
+ // HttpPkg provides an interface for making HTTP requests,
+ // facilitating communication with external APIs or services.
+ HttpPkg api.HttpPkgInterface
+}
+
+// MetricsResponse represents a mapping of metric names to their integer values.
+type MetricsResponse map[string]int
+
+// parseMetricsContent parses Prometheus-style metrics content and returns a mapping of metric names to their integer values.
+// It validates the input format, extracts key-value pairs, and handles errors during parsing.
+func parseMetricsContent(content string) (MetricsResponse, error) {
+ var err error
+ result := MetricsResponse{}
+
+ // Validate the Content
+ var isValid bool
+ isValid, err = regexp.MatchString(`^#`, content)
+ isValid = isValid && err == nil && content != ""
+ if !isValid {
+ return nil, errors.New("invalid metrics content")
+ }
+
+ // Regex for Metrics Format,
+ // selects all content that does not start with # in multiline mode
+ re := regexp.MustCompile(`(?m)^[^#].*`)
+ rows := re.FindAllString(content, -1)
+
+ // Add the strings to the map
+ for _, row := range rows {
+ var value int
+ keyValue := strings.Split(row, " ")
+ value, err = strconv.Atoi(keyValue[1])
+ result[keyValue[0]] = value
+ }
+
+ // Handle any error results
+ if err != nil {
+ return nil, err
+ }
+
+ // Give the user what they asked for
+ return result, nil
+}
+
+// Get retrieves metrics data, processes network statistics,
+// calculates TPS and round time, and updates the Metrics state.
+func (m Metrics) Get(ctx context.Context, currentRound uint64) (Metrics, api.ResponseInterface, error) {
+ response, err := m.Client.MetricsWithResponse(ctx)
+ // Handle Errors and Status
+ if err != nil {
+ m.Enabled = false
+ return m, response, err
+ }
+ if response.StatusCode() != 200 {
+ m.Enabled = false
+ return m, response, errors.New(InvalidStatus)
+ }
+
+ // Parse the Metrics Endpoint
+ content, err := parseMetricsContent(string(response.Body))
+ if err != nil {
+ m.Enabled = false
+ return m, response, err
+ }
+
+ // Handle Metrics
+ m.Enabled = true
+ now := time.Now()
+ diff := now.Sub(m.LastTS)
+
+ m.TX = max(0, int(float64(content["algod_network_sent_bytes_total"]-m.LastTX)/diff.Seconds()))
+ m.RX = max(0, int(float64(content["algod_network_received_bytes_total"]-m.LastRX)/diff.Seconds()))
+
+ m.LastTS = now
+ m.LastTX = content["algod_network_sent_bytes_total"]
+ m.LastRX = content["algod_network_received_bytes_total"]
+
+ if int(currentRound) > m.Window {
+ var blockMetrics BlockMetrics
+ var blockMetricsResponse api.ResponseInterface
+ blockMetrics, blockMetricsResponse, err = GetBlockMetrics(ctx, m.Client, currentRound, m.Window)
+ if err != nil {
+ return m, blockMetricsResponse, err
+ }
+ m.TPS = blockMetrics.TPS
+ m.RoundTime = blockMetrics.AvgTime
+ }
+
+ return m, response, nil
+}
+
+// NewMetrics initializes and retrieves Metrics data by fetching the current round's metrics from the provided client.
+// It requires a context, API client, HTTP package interface, and the current round number as inputs.
+// Returns the populated Metrics instance, an API response interface, and an error, if any occurs.
+func NewMetrics(ctx context.Context, client api.ClientWithResponsesInterface, httpPkg api.HttpPkgInterface, currentRound uint64) (Metrics, api.ResponseInterface, error) {
+ return Metrics{
+ Enabled: false,
+ Window: 100,
+ RoundTime: 0 * time.Second,
+ TPS: 0,
+ RX: 0,
+ TX: 0,
+ LastTS: time.Time{},
+ LastRX: 0,
+
+ Client: client,
+ HttpPkg: httpPkg,
+ }.Get(ctx, currentRound)
+}
diff --git a/internal/metrics_test.go b/internal/algod/metrics_test.go
similarity index 79%
rename from internal/metrics_test.go
rename to internal/algod/metrics_test.go
index 7cf091d9..0b894fd1 100644
--- a/internal/metrics_test.go
+++ b/internal/algod/metrics_test.go
@@ -1,32 +1,29 @@
-package internal
+package algod
import (
"context"
- "github.com/algorandfoundation/algorun-tui/internal/test"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test"
"strconv"
"testing"
)
func Test_GetMetrics(t *testing.T) {
client := test.GetClient(true)
-
- metrics, err := GetMetrics(context.Background(), client)
+ httpPkg := new(api.HttpPkg)
+ metrics, _, err := NewMetrics(context.Background(), client, httpPkg, 0)
if err == nil {
t.Error("error expected")
}
- client = test.GetClient(false)
- metrics, err = GetMetrics(context.Background(), client)
+ metrics.Client = test.GetClient(false)
+ metrics, _, err = metrics.Get(context.Background(), 0)
if err != nil {
t.Fatal(err)
}
- if metrics["algod_agreement_dropped"] != 0 {
- t.Fatal(strconv.Itoa(metrics["algod_agreement_dropped"]) + " is not zero")
- }
-
- client = test.NewClient(false, true)
- metrics, err = GetMetrics(context.Background(), client)
+ metrics.Client = test.NewClient(false, true)
+ _, _, err = metrics.Get(context.Background(), 10)
if err == nil {
t.Error("expected error")
}
diff --git a/internal/algod/msgs/errors.go b/internal/algod/msgs/errors.go
new file mode 100644
index 00000000..c8e77744
--- /dev/null
+++ b/internal/algod/msgs/errors.go
@@ -0,0 +1,4 @@
+package msgs
+
+// InvalidDataDirectory indicates that the specified Algorand data directory is invalid or improperly configured.
+const InvalidDataDirectory = "algorand data directory is invalid"
diff --git a/internal/algod/participation/participation.go b/internal/algod/participation/participation.go
new file mode 100644
index 00000000..646cc7b2
--- /dev/null
+++ b/internal/algod/participation/participation.go
@@ -0,0 +1,221 @@
+package participation
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/algorandfoundation/nodekit/api"
+)
+
+// RangeType represents a type of range, such as time-based or round-based, used in participation key generation.
+type RangeType string
+
+const (
+
+ // TimeRange defines a range type measured in seconds, used for specifying time-based participation key validity.
+ TimeRange RangeType = "seconds"
+
+ // RoundRange represents a range type defined in terms of rounds for participation key validity.
+ RoundRange RangeType = "rounds"
+)
+
+// List represents a collection of api.ParticipationKey elements.
+type List []api.ParticipationKey
+
+// GetList get the participation keys from the node
+func GetList(ctx context.Context, client api.ClientWithResponsesInterface) (List, api.ResponseInterface, error) {
+ partKeysResponse, err := client.GetParticipationKeysWithResponse(ctx)
+ if err != nil {
+ return nil, partKeysResponse, err
+ }
+ if partKeysResponse.StatusCode() != 200 {
+ return nil, partKeysResponse, errors.New(partKeysResponse.Status())
+ }
+ return *partKeysResponse.JSON200, partKeysResponse, err
+}
+
+// GetKey get a specific participation key by id
+func GetKey(ctx context.Context, client api.ClientWithResponsesInterface, participationId string) (*api.ParticipationKey, api.ResponseInterface, error) {
+ keyResponse, err := client.GetParticipationKeyByIDWithResponse(ctx, participationId)
+ if err != nil {
+ return nil, keyResponse, err
+ }
+ if keyResponse.StatusCode() != 200 {
+ return nil, keyResponse, errors.New(keyResponse.Status())
+ }
+ return keyResponse.JSON200, keyResponse, err
+}
+
+// GenerateKeys creates a participation keypair for the specified address and validity period parameters.
+// It ensures the key creation on the node is complete and verifies the key's properties before returning it.
+// If the key creation fails, or the operation is interrupted/timed out, it returns an appropriate error.
+func GenerateKeys(
+ ctx context.Context,
+ client api.ClientWithResponsesInterface,
+ address string,
+ params *api.GenerateParticipationKeysParams,
+) (*api.ParticipationKey, error) {
+ // Generate a new keypair
+ key, err := client.GenerateParticipationKeysWithResponse(ctx, address, params)
+ if err != nil {
+ return nil, err
+ }
+ if key.StatusCode() != 200 {
+ if key.JSON400 != nil {
+ return nil, errors.New(key.JSON400.Message)
+ }
+
+ status := key.Status()
+ if status != "" {
+ return nil, errors.New(status)
+ }
+ return nil, errors.New("something went wrong")
+ }
+
+ // ๐ - Zero 2024
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, context.Canceled
+ case <-time.After(2 * time.Second):
+ partKeys, _, err := GetList(ctx, client)
+ if partKeys == nil || err != nil {
+ return nil, errors.New("failed to get participation keys")
+ }
+ for _, k := range partKeys {
+ if k.Address == address &&
+ k.Key.VoteFirstValid == params.First &&
+ k.Key.VoteLastValid == params.Last {
+ return &k, nil
+ }
+ }
+ case <-time.After(20 * time.Minute):
+ return nil, errors.New("timeout waiting for key to be created")
+ }
+ }
+}
+
+// Delete remove a key from the node
+func Delete(ctx context.Context, client api.ClientWithResponsesInterface, participationId string) error {
+ deletion, err := client.DeleteParticipationKeyByIDWithResponse(ctx, participationId)
+ if err != nil {
+ return err
+ }
+ if deletion.StatusCode() != 200 {
+ return errors.New(deletion.Status())
+ }
+ return nil
+}
+
+// RemovePartKeyByID Removes a participation key from the list of keys
+func RemovePartKeyByID(slice *List, id string) {
+ for i, item := range *slice {
+ if item.Id == id {
+ *slice = append((*slice)[:i], (*slice)[i+1:]...)
+ return
+ }
+ }
+}
+
+// FindParticipationIdForVoteKey searches a List for an item's VoteParticipationKey matching the given votekey and returns its ID.
+// It returns a pointer to the ID as a string if a match is found, or nil otherwise.
+func FindParticipationIdForVoteKey(slice List, votekey []byte) *string {
+ for _, item := range slice {
+ if string(item.Key.VoteParticipationKey) == string(votekey) {
+ return &item.Id
+ }
+ }
+ return nil
+}
+
+// IsActive checks if the given participation key matches the account's registered participation details and is valid.
+func IsActive(part api.ParticipationKey, account api.AccountParticipation) bool {
+ var equal = false
+ if bytes.Equal(part.Key.VoteParticipationKey, account.VoteParticipationKey) &&
+ part.Key.VoteLastValid == account.VoteLastValid &&
+ part.Key.VoteFirstValid == account.VoteFirstValid {
+ equal = true
+ }
+ return equal
+}
+
+// OnlineShortLinkBody represents the request payload for creating an online short link.
+// It contains account details, cryptographic keys in Base64, validity range, key dilution, and network information.
+type OnlineShortLinkBody struct {
+ Account string `json:"account"`
+ VoteKeyB64 string `json:"voteKeyB64"`
+ SelectionKeyB64 string `json:"selectionKeyB64"`
+ StateProofKeyB64 string `json:"stateProofKeyB64"`
+ VoteFirstValid int `json:"voteFirstValid"`
+ VoteLastValid int `json:"voteLastValid"`
+ KeyDilution int `json:"keyDilution"`
+ Network string `json:"network"`
+}
+
+// GetOnlineShortLink sends a POST request to create an online short link
+// and returns the response or an error if it occurs.
+func GetOnlineShortLink(http api.HttpPkgInterface, part OnlineShortLinkBody) (ShortLinkResponse, error) {
+ var response ShortLinkResponse
+ data, err := json.Marshal(part)
+ if err != nil {
+ return response, err
+ }
+ res, err := http.Post("http://b.nodekit.run/online", "applicaiton/json", bytes.NewReader(data))
+ if err != nil {
+ return response, err
+ }
+ defer res.Body.Close()
+
+ err = json.NewDecoder(res.Body).Decode(&response)
+ if err != nil {
+ return response, err
+ }
+
+ return response, nil
+}
+
+// ShortLinkResponse represents the response structure for a shortened link,
+// containing its unique identifier.
+type ShortLinkResponse struct {
+ Id string `json:"id"`
+}
+
+// OfflineShortLinkBody represents the request body for creating an
+// offline short link containing an address and network.
+type OfflineShortLinkBody struct {
+ Account string `json:"account"`
+ Network string `json:"network"`
+}
+
+// GetOfflineShortLink sends an OnlineShortLinkBody to create an offline short link and returns the corresponding response.
+// Uses the provided HttpPkgInterface for the POST request and handles JSON encoding/decoding of request and response.
+// Returns an OfflineShortLinkResponse on success or an error if the operation fails.
+func GetOfflineShortLink(http api.HttpPkgInterface, offline OfflineShortLinkBody) (ShortLinkResponse, error) {
+ var response ShortLinkResponse
+ data, err := json.Marshal(offline)
+ if err != nil {
+ return response, err
+ }
+ res, err := http.Post("http://b.nodekit.run/offline", "applicaiton/json", bytes.NewReader(data))
+ if err != nil {
+ return response, err
+ }
+ defer res.Body.Close()
+
+ err = json.NewDecoder(res.Body).Decode(&response)
+ if err != nil {
+ return response, err
+ }
+
+ return response, nil
+}
+
+// ToShortLink generates a shortened URL string using the unique
+// identifier from the provided ShortLinkResponse.
+func ToShortLink(link ShortLinkResponse) string {
+ return fmt.Sprintf("https://b.nodekit.run/%s", link.Id)
+}
diff --git a/internal/algod/participation/participation_test.go b/internal/algod/participation/participation_test.go
new file mode 100644
index 00000000..24daf174
--- /dev/null
+++ b/internal/algod/participation/participation_test.go
@@ -0,0 +1,263 @@
+package participation
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test"
+ "io"
+ "net/http"
+ "testing"
+)
+
+func Test_ListParticipationKeys(t *testing.T) {
+ ctx := context.Background()
+ client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, _, err = GetList(ctx, client)
+
+ // Expect unauthorized for Urtho servers
+ if err == nil {
+ t.Fatal(err)
+ }
+
+ // Setup elevated client
+ tClient := test.GetClient(false)
+
+ keys, _, err := GetList(ctx, tClient)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fmt.Println(keys)
+}
+
+func Test_ReadParticipationKey(t *testing.T) {
+ ctx := context.Background()
+ client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, _, err = GetKey(ctx, client, "unknown")
+
+ // Expect unauthorized for Urtho servers
+ if err == nil {
+ t.Fatal(err)
+ }
+
+ tClient := test.GetClient(false)
+
+ keys, _, err := GetList(ctx, tClient)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if keys == nil {
+ t.Fatal(err)
+ }
+
+ _, _, err = GetKey(ctx, tClient, (keys)[0].Id)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+}
+
+func Test_GenerateParticipationKey(t *testing.T) {
+ ctx := context.Background()
+
+ // Create Client
+ client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Generate error
+ _, err = GenerateKeys(ctx, client, "", nil)
+ if err == nil {
+ t.Fatal(err)
+ }
+
+ // Setup test client
+ tClient := test.GetClient(false)
+
+ params := api.GenerateParticipationKeysParams{
+ Dilution: nil,
+ First: 0,
+ Last: 30,
+ }
+
+ // This returns nothing and sucks
+ key, err := GenerateKeys(ctx, tClient, "ABC", ¶ms)
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Println(key)
+}
+
+func Test_DeleteParticipationKey(t *testing.T) {
+ ctx := context.Background()
+
+ client := test.GetClient(false)
+ params := api.GenerateParticipationKeysParams{
+ Dilution: nil,
+ First: 0,
+ Last: 30000,
+ }
+ key, err := GenerateKeys(ctx, client, "ABC", ¶ms)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = Delete(ctx, client, key.Id)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+func Test_RemovePartKeyByID(t *testing.T) {
+ // Test case: Remove an existing key
+ t.Run("Remove existing key", func(t *testing.T) {
+ keys := List{
+ {Id: "key1"},
+ {Id: "key2"},
+ {Id: "key3"},
+ }
+ expectedKeys := List{
+ {Id: "key1"},
+ {Id: "key3"},
+ }
+ RemovePartKeyByID(&keys, "key2")
+ if len(keys) != len(expectedKeys) {
+ t.Fatalf("expected %d keys, got %d", len(expectedKeys), len(keys))
+ }
+ for i, key := range keys {
+ if key.Id != expectedKeys[i].Id {
+ t.Fatalf("expected key ID %s, got %s", expectedKeys[i].Id, key.Id)
+ }
+ }
+ })
+
+ // Test case: Remove a non-existing key
+ t.Run("Remove non-existing key", func(t *testing.T) {
+ keys := List{
+ {Id: "key1"},
+ {Id: "key2"},
+ {Id: "key3"},
+ }
+ expectedKeys := List{
+ {Id: "key1"},
+ {Id: "key2"},
+ {Id: "key3"},
+ }
+ RemovePartKeyByID(&keys, "key4")
+ if len(keys) != len(expectedKeys) {
+ t.Fatalf("expected %d keys, got %d", len(expectedKeys), len(keys))
+ }
+ for i, key := range keys {
+ if key.Id != expectedKeys[i].Id {
+ t.Fatalf("expected key ID %s, got %s", expectedKeys[i].Id, key.Id)
+ }
+ }
+ })
+
+ // Test case: Remove a key from an empty list
+ t.Run("Remove key from empty list", func(t *testing.T) {
+ keys := List{}
+ RemovePartKeyByID(&keys, "key1")
+ if len(keys) != 0 {
+ t.Fatalf("expected 0 keys, got %d", len(keys))
+ }
+ })
+}
+
+var onlineShortLinkResponseStr = `{
+ "id": "WKIPKTWIFZQJ2"
+ }`
+
+type testOnlineShortner struct {
+ api.HttpPkgInterface
+}
+
+// TODO: toggle between Unit/Integration tests
+func (testOnlineShortner) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
+ responseBody := io.NopCloser(bytes.NewReader([]byte(onlineShortLinkResponseStr)))
+ return &http.Response{
+ Status: "",
+ StatusCode: 0,
+ Proto: "",
+ ProtoMajor: 0,
+ ProtoMinor: 0,
+ Header: nil,
+ Body: responseBody,
+ ContentLength: 0,
+ TransferEncoding: nil,
+ Close: false,
+ Uncompressed: false,
+ Trailer: nil,
+ Request: nil,
+ TLS: nil,
+ }, nil
+}
+func Test_ToOnlineShortLink(t *testing.T) {
+ link, err := GetOnlineShortLink(new(testOnlineShortner), OnlineShortLinkBody{
+ Account: "JPEGRZ6G4IBZCOC7UV6QZWJ6TENNKRIPENUJTLG5K7PKIKMVTJHUGERARE",
+ VoteKeyB64: "WWHePYtNZ2T3sHkqdd/38EvoFWrnIKPrTo6xN/4T1l4=",
+ SelectionKeyB64: "e4kBLu7zXOorjLVzJHOiAn+IhOBsYBCqqHKaJCiCdJs=",
+ StateProofKeyB64: "1GdNPOck+t6yXvuXxrDEPKqgi4I2sTaNugV1kd5ksUW2G1U6x1FT0WR3aT3ZSSmbYoDt3cVrB3vIPJA8GkqSYg==",
+ VoteFirstValid: 3118965,
+ VoteLastValid: 3148965,
+ KeyDilution: 995,
+ Network: "mainnet",
+ })
+ if err != nil {
+ t.Error(err)
+ }
+ if link.Id != "WKIPKTWIFZQJ2" {
+ t.Error("Link should be a known hash")
+ }
+}
+
+var offlineShortLinkResponseStr = `{
+ "id": "D3O3GEG2UD2GW"
+ }`
+
+type testOfflineShortner struct {
+ api.HttpPkgInterface
+}
+
+// TODO: toggle between Unit/Integration tests
+func (testOfflineShortner) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) {
+ responseBody := io.NopCloser(bytes.NewReader([]byte(offlineShortLinkResponseStr)))
+ return &http.Response{
+ Status: "",
+ StatusCode: 0,
+ Proto: "",
+ ProtoMajor: 0,
+ ProtoMinor: 0,
+ Header: nil,
+ Body: responseBody,
+ ContentLength: 0,
+ TransferEncoding: nil,
+ Close: false,
+ Uncompressed: false,
+ Trailer: nil,
+ Request: nil,
+ TLS: nil,
+ }, nil
+}
+func Test_ToOfflineShortLink(t *testing.T) {
+ link, err := GetOfflineShortLink(new(testOfflineShortner), OfflineShortLinkBody{
+ Account: "JPEGRZ6G4IBZCOC7UV6QZWJ6TENNKRIPENUJTLG5K7PKIKMVTJHUGERARE",
+ Network: "mainnet",
+ })
+ if err != nil {
+ t.Error(err)
+ }
+ if link.Id != "D3O3GEG2UD2GW" {
+ t.Error("Link should be a known hash")
+ }
+}
diff --git a/internal/algod/state.go b/internal/algod/state.go
new file mode 100644
index 00000000..ac4bc8c9
--- /dev/null
+++ b/internal/algod/state.go
@@ -0,0 +1,193 @@
+package algod
+
+import (
+ "context"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "time"
+
+ "github.com/algorandfoundation/nodekit/api"
+)
+
+// StateModel represents the state of the application,
+// including status, metrics, accounts, keys, and other configurations.
+type StateModel struct {
+
+ // Status represents the current status of the algod node,
+ // including network state and round information.
+ Status Status
+
+ // Metrics provides runtime statistics including
+ // round time, transactions per second, and data transfer metrics.
+ Metrics Metrics
+
+ // Accounts holds a mapping of account identifiers to their corresponding Account details.
+ // This map is derived from the list of the type api.ParticipationKey
+ Accounts map[string]Account
+
+ // ParticipationKeys is a slice of participation keys used by the node
+ // to interact with the blockchain and consensus protocol.
+ ParticipationKeys participation.List
+
+ // Admin indicates whether the current node has
+ // admin privileges or capabilities enabled.
+ Admin bool
+
+ // Watching indicates whether the StateModel is actively monitoring
+ // changes or processes in a background loop.
+ // TODO: handle contexts instead of adding it to state (skill-issue zero)
+ Watching bool
+
+ // Client provides an interface for interacting with API endpoints,
+ // enabling various node operations and data retrieval.
+ Client api.ClientWithResponsesInterface
+ // HttpPkg provides an interface for making HTTP requests,
+ // enabling communication with external APIs or services.
+ HttpPkg api.HttpPkgInterface
+
+ // Context provides a context for managing cancellation,
+ // deadlines, and request-scoped values in StateModel operations.
+ // TODO: implement more of the context
+ Context context.Context
+}
+
+// NewStateModel initializes and returns a new StateModel instance
+// along with an API response and potential error.
+func NewStateModel(ctx context.Context, client api.ClientWithResponsesInterface, httpPkg api.HttpPkgInterface) (*StateModel, api.ResponseInterface, error) {
+ // Preload the node status
+ status, response, err := NewStatus(ctx, client, httpPkg)
+ if err != nil {
+ return nil, response, err
+ }
+ // Try to fetch the latest metrics
+ metrics, response, err := NewMetrics(ctx, client, httpPkg, status.LastRound)
+ if err != nil {
+ return nil, response, err
+ }
+
+ partKeys, partkeysResponse, err := participation.GetList(ctx, client)
+
+ return &StateModel{
+ Status: status,
+ Metrics: metrics,
+ Accounts: ParticipationKeysToAccounts(partKeys),
+ ParticipationKeys: partKeys,
+
+ Admin: true,
+ Watching: true,
+
+ Client: client,
+ HttpPkg: httpPkg,
+ Context: ctx,
+ }, partkeysResponse, nil
+}
+
+// waitAfterError updates the state to "DOWN", invokes the callback with an error, and pauses for a fixed duration if an error occurs.
+// TODO: handle in context loop
+func (s *StateModel) waitAfterError(err error, cb func(model *StateModel, err error)) {
+ if err != nil {
+ s.Status.State = "DOWN"
+ cb(nil, err)
+ time.Sleep(time.Second * 3)
+ }
+}
+
+// Watch starts monitoring the state in a loop, invoking a callback with updates or errors as they occur.
+// TODO: allow context to handle loop
+func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Context, t system.Time) {
+ var err error
+
+ // Setup Defaults
+ s.Watching = true
+ if s.Metrics.Window == 0 {
+ s.Metrics.Window = 100
+ }
+
+ // Fetch the latest Status
+ s.Status, _, err = s.Status.Get(ctx)
+ if err != nil {
+ // callback immediately on error
+ cb(nil, err)
+ }
+
+ // The main Loop
+ // TODO: Refactor to Context
+ for {
+ if !s.Watching {
+ break
+ }
+ // Abort on Fast-Catchup
+ if s.Status.State == FastCatchupState {
+ // Update current render
+ cb(s, nil)
+ // Wait for a while
+ time.Sleep(time.Second * 2)
+ // Check status
+ s.Status, _, err = s.Status.Get(ctx)
+ // Report errors
+ if err != nil {
+ cb(nil, err)
+ }
+ // Update render after status fetch
+ cb(s, nil)
+ continue
+ }
+
+ // Wait for the next block
+ s.Status, _, err = s.Status.Wait(ctx)
+ s.waitAfterError(err, cb)
+ if err != nil {
+ continue
+ }
+
+ // Fetch Keys
+ s.UpdateKeys(ctx, t)
+
+ if s.Status.State == SyncingState {
+ cb(s, nil)
+ continue
+ }
+ // Run Round Averages and RX/TX every 5 rounds
+ if s.Status.LastRound%5 == 0 {
+ s.Metrics, _, err = s.Metrics.Get(ctx, s.Status.LastRound)
+ s.waitAfterError(err, cb)
+ if err != nil {
+ continue
+ }
+ }
+
+ // Callback the current state to the app
+ cb(s, nil)
+ }
+}
+
+// Stop halts the monitoring process by setting the Watching field to false.
+func (s *StateModel) Stop() {
+ s.Watching = false
+}
+
+// UpdateKeys retrieves and updates participation keys, manages admin status, and synchronizes account data with the node.
+func (s *StateModel) UpdateKeys(ctx context.Context, t system.Time) {
+ var err error
+ s.ParticipationKeys, _, err = participation.GetList(ctx, s.Client)
+ if err != nil {
+ s.Admin = false
+ }
+ if err == nil {
+ s.Admin = true
+ s.Accounts = ParticipationKeysToAccounts(s.ParticipationKeys)
+ for _, acct := range s.Accounts {
+ // For each account, update the data from the RPC endpoint
+ if s.Status.State == StableState {
+ // Skip eon errors
+ rpcAcct, err := GetAccount(s.Client, acct.Address)
+ if err != nil {
+ continue
+ }
+
+ s.Accounts[acct.Address] = s.Accounts[acct.Address].Merge(rpcAcct)
+ s.Accounts[acct.Address] = s.Accounts[acct.Address].UpdateExpiredTime(t, s.ParticipationKeys, int(s.Status.LastRound), s.Metrics.RoundTime)
+ }
+ }
+ }
+}
diff --git a/internal/state_test.go b/internal/algod/state_test.go
similarity index 79%
rename from internal/state_test.go
rename to internal/algod/state_test.go
index a3f19e45..e8118d1c 100644
--- a/internal/state_test.go
+++ b/internal/algod/state_test.go
@@ -1,33 +1,39 @@
-package internal
+package algod
import (
"context"
- "github.com/algorandfoundation/algorun-tui/api"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
"testing"
"time"
)
func Test_StateModel(t *testing.T) {
+ t.Skip()
// Setup elevated client
apiToken, err := securityprovider.NewSecurityProviderApiKey("header", "X-Algo-API-Token", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
if err != nil {
t.Fatal(err)
}
client, err := api.NewClientWithResponses("http://localhost:8080", api.WithRequestEditorFn(apiToken.Intercept))
-
+ httpPkg := new(api.HttpPkg)
state := StateModel{
Watching: true,
- Status: StatusModel{
+ Status: Status{
LastRound: 1337,
NeedsUpdate: true,
State: SyncingState,
+ Client: client,
+ HttpPkg: httpPkg,
},
- Metrics: MetricsModel{
+ Metrics: Metrics{
RoundTime: 0,
TX: 0,
RX: 0,
TPS: 0,
+ Client: client,
+ HttpPkg: httpPkg,
},
Client: client,
Context: context.Background(),
@@ -39,7 +45,7 @@ func Test_StateModel(t *testing.T) {
return
}
count++
- }, context.Background(), client)
+ }, context.Background(), new(mock.Clock))
time.Sleep(5 * time.Second)
// Stop the watcher
state.Stop()
diff --git a/internal/algod/status.go b/internal/algod/status.go
new file mode 100644
index 00000000..01cace6c
--- /dev/null
+++ b/internal/algod/status.go
@@ -0,0 +1,178 @@
+package algod
+
+import (
+ "context"
+ "errors"
+
+ "github.com/algorandfoundation/nodekit/api"
+)
+
+// InvalidStatus indicates an error when a response contains an invalid or unexpected status code.
+const InvalidStatus = "invalid status"
+
+// State represents the operational state of a process or system within the application. It is defined as a string type.
+type State string
+
+const (
+
+ // FastCatchupState represents the state when the system is performing a fast catchup operation to synchronize.
+ FastCatchupState State = "FAST-CATCHUP"
+
+ // SyncingState represents the state where the system is in the process of synchronizing to the latest data or state.
+ SyncingState State = "SYNCING"
+
+ // StableState indicates the system is in a stable and operational state with no ongoing synchronization or major updates.
+ StableState State = "RUNNING"
+)
+
+// Status represents the state of a system including metadata like version, network, and operational state.
+type Status struct {
+
+ // State represents the operational state of a process or system, defined as a string.
+ State State `json:"state"`
+
+ // Version represents the version identifier of the system, typically used to denote the current software version.
+ Version string `json:"version"`
+
+ // Network represents the name of the network the status is associated with.
+ Network string `json:"network"`
+
+ // Voting indicates whether a node participated in the current upgrade voting process.
+ Voting bool `json:"voting"`
+
+ // NeedsUpdate indicates whether the system requires an update based on the current version and available release data.
+ NeedsUpdate bool `json:"needsUpdate"`
+
+ // LastRound represents the most recent round number recorded by the system or client.
+ LastRound uint64 `json:"lastRound"`
+
+ // Catchpoint is a pointer to a string that identifies the current catchpoint for node synchronization or fast catchup.
+ Catchpoint *string `json:"catchpoint"`
+ CatchpointAccountsTotal int `json:"catchpointAccountsTotal"`
+ CatchpointAccountsProcessed int `json:"catchpointAccountsProcessed"`
+ CatchpointAccountsVerified int `json:"catchpointAccountsVerified"`
+ CatchpointKeyValueTotal int `json:"catchpointKeyValueTotal"`
+ CatchpointKeyValueProcessed int `json:"catchpointKeyValueProcessed"`
+ CatchpointKeyValueVerified int `json:"catchpointKeyValueVerified"`
+ CatchpointBlocksTotal int `json:"catchpointTotalBlocks"`
+ CatchpointBlocksAcquired int `json:"catchpointAcquiredBlocks"`
+
+ SyncTime int `json:"syncTime"`
+
+ // Client provides methods for interacting with the API, adhering to ClientWithResponsesInterface specifications.
+ Client api.ClientWithResponsesInterface `json:"-"`
+
+ // HttpPkg represents an interface for HTTP package operations, providing methods for making HTTP requests.
+ HttpPkg api.HttpPkgInterface `json:"-"`
+}
+
+// Update synchronizes non-identical fields between two Status instances and returns the updated Status.
+func (s Status) Update(status Status) Status {
+ if s.State != status.State {
+ s.State = status.State
+ }
+ if s.Version != status.Version {
+ s.Version = status.Version
+ }
+ if s.Network != status.Network {
+ s.Network = status.Network
+ }
+ if s.Voting != status.Voting {
+ s.Voting = status.Voting
+ }
+ if s.NeedsUpdate != status.NeedsUpdate {
+ s.NeedsUpdate = status.NeedsUpdate
+ }
+ if s.LastRound != status.LastRound {
+ s.LastRound = status.LastRound
+ }
+ return s
+}
+
+// Wait waits for the next block round based on the current LastRound and updates the Status with the returned response.
+// It interacts with the client's WaitForBlockWithResponse method and handles any errors or invalid status codes.
+// Returns the updated Status, the response object, or an error if the operation fails.
+func (s Status) Wait(ctx context.Context) (Status, api.ResponseInterface, error) {
+ response, err := s.Client.WaitForBlockWithResponse(ctx, int(s.LastRound))
+ if err != nil {
+ return s, response, err
+ }
+ if response.StatusCode() >= 300 {
+ return s, response, errors.New(InvalidStatus)
+ }
+
+ return s.Merge(*response.JSON200), response, nil
+}
+
+// Merge updates the current Status with data from a given StatusLike instance and adjusts fields based on defined conditions.
+func (s Status) Merge(res api.StatusLike) Status {
+ s.LastRound = uint64(res.LastRound)
+ catchpoint := res.Catchpoint
+ if catchpoint != nil && *catchpoint != "" {
+ s.State = FastCatchupState
+ s.Catchpoint = catchpoint
+ s.SyncTime = res.CatchupTime
+ s.CatchpointAccountsTotal = *res.CatchpointTotalAccounts
+ s.CatchpointAccountsProcessed = *res.CatchpointProcessedAccounts
+ s.CatchpointAccountsVerified = *res.CatchpointVerifiedAccounts
+ s.CatchpointKeyValueTotal = *res.CatchpointTotalKvs
+ s.CatchpointKeyValueProcessed = *res.CatchpointProcessedKvs
+ s.CatchpointKeyValueVerified = *res.CatchpointVerifiedKvs
+ s.CatchpointBlocksAcquired = *res.CatchpointAcquiredBlocks
+ s.CatchpointBlocksTotal = *res.CatchpointTotalBlocks
+ } else if res.CatchupTime > 0 {
+ s.SyncTime = res.CatchupTime
+ s.State = SyncingState
+ } else {
+ s.State = StableState
+ }
+
+ if res.UpgradeNodeVote != nil {
+ s.Voting = true
+ }
+ return s
+}
+
+// Get retrieves the current system status by invoking the client's GetStatusWithResponse method and merging the result.
+// It returns the updated Status, the API response, or an error if the request fails or the status code is invalid.
+func (s Status) Get(ctx context.Context) (Status, api.ResponseInterface, error) {
+ statusResponse, err := s.Client.GetStatusWithResponse(ctx)
+ if err != nil {
+ return s, statusResponse, err
+ }
+ if statusResponse.StatusCode() >= 300 {
+ return s, statusResponse, errors.New(InvalidStatus)
+ }
+ return s.Merge(*statusResponse.JSON200), statusResponse, nil
+}
+
+// NewStatus initializes and returns a Status object based on the provided context, client, and HTTP package interface.
+// The function also checks for system updates and merges the current status with the latest available data.
+func NewStatus(ctx context.Context, client api.ClientWithResponsesInterface, httpPkg api.HttpPkgInterface) (Status, api.ResponseInterface, error) {
+ var status Status
+ status.Client = client
+ status.HttpPkg = httpPkg
+
+ v, versionResponse, err := GetVersion(ctx, client)
+ if err != nil {
+ return status, versionResponse.(api.ResponseInterface), err
+ }
+ status.Network = v.Network
+ status.Version = v.Version
+ status.NeedsUpdate = false
+
+ if v.Channel == "beta" || v.Channel == "stable" {
+ // TODO: last checked
+ releaseResponse, err := api.GetGoAlgorandReleaseWithResponse(httpPkg, v.Channel)
+ // Return the error and response
+ if err != nil {
+ return status, releaseResponse, err
+ }
+ // Update status update field
+ if releaseResponse != nil && status.Version != releaseResponse.JSON200 {
+ status.NeedsUpdate = true
+ }
+ }
+
+ return status.Get(ctx)
+}
diff --git a/internal/algod/status_test.go b/internal/algod/status_test.go
new file mode 100644
index 00000000..81d4e779
--- /dev/null
+++ b/internal/algod/status_test.go
@@ -0,0 +1,83 @@
+package algod
+
+import (
+ "context"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test"
+ "testing"
+)
+
+func Test_StatusModel(t *testing.T) {
+ m := Status{LastRound: 0}
+
+ emptyCatchpoint := ""
+
+ m = m.Merge(api.StatusLike{LastRound: 5, Catchpoint: &emptyCatchpoint, CatchupTime: 10})
+ if m.LastRound != 5 {
+ t.Errorf("expected LastRound: 5, got %d", m.LastRound)
+ }
+ if m.State != SyncingState {
+ t.Errorf("expected State: %s, got %s", SyncingState, m.State)
+ }
+
+ m = m.Merge(api.StatusLike{LastRound: 10, Catchpoint: &emptyCatchpoint, CatchupTime: 0})
+ if m.LastRound != 10 {
+ t.Errorf("expected LastRound: 10, got %d", m.LastRound)
+ }
+ if m.State != StableState {
+ t.Errorf("expected State: %s, got %s", StableState, m.State)
+ }
+
+ catchpoint := "catchpoint"
+ accountsTotal := 1000000
+ processedAccounts := 0
+ verifiedAccounts := 0
+ keyValueTotal := 1000
+ keyValueProcessed := 0
+ keyValueVerified := 0
+ acquiredBlocks := 1000000
+ blocksTotal := 10000000
+ m = m.Merge(api.StatusLike{
+ LastRound: 10,
+ Catchpoint: &catchpoint,
+ CatchupTime: 0,
+ CatchpointAcquiredBlocks: &acquiredBlocks,
+ CatchpointTotalBlocks: &blocksTotal,
+ CatchpointTotalAccounts: &accountsTotal,
+ CatchpointVerifiedAccounts: &verifiedAccounts,
+ CatchpointProcessedAccounts: &processedAccounts,
+ CatchpointTotalKvs: &keyValueTotal,
+ CatchpointProcessedKvs: &keyValueProcessed,
+ CatchpointVerifiedKvs: &keyValueVerified,
+ })
+ if m.State != FastCatchupState {
+ t.Errorf("expected State: %s, got %s", FastCatchupState, m.State)
+ }
+
+}
+
+func Test_StatusFetch(t *testing.T) {
+ client := test.GetClient(true)
+ httpPkg := new(api.HttpPkg)
+
+ m, _, err := NewStatus(context.Background(), client, httpPkg)
+ if err == nil {
+ t.Error("expected error, got nil")
+ }
+
+ client = test.NewClient(false, true)
+ m, _, err = NewStatus(context.Background(), client, httpPkg)
+ if err == nil {
+ t.Error("expected error, got nil")
+ }
+
+ client = test.GetClient(false)
+ m, _, err = NewStatus(context.Background(), client, httpPkg)
+ if err != nil {
+ t.Error(err)
+ }
+ if m.LastRound == 0 {
+ t.Error("expected LastRound to be non-zero")
+ }
+
+}
diff --git a/internal/algod/utils/utils.go b/internal/algod/utils/utils.go
new file mode 100644
index 00000000..18e8955f
--- /dev/null
+++ b/internal/algod/utils/utils.go
@@ -0,0 +1,169 @@
+package utils
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/system"
+ "github.com/spf13/cobra"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const AlgodNetEndpointFileMissingAddress = "missing://endpoint"
+
+type DataFolderConfig struct {
+ Path string `json:"path"`
+ Token string `json:"token"`
+ Endpoint string `json:"endpoint"`
+ Network string `json:"network"`
+ PID int `json:"PID"`
+}
+
+func ToDataFolderConfig(path string) (DataFolderConfig, error) {
+ var dataFolderConfig DataFolderConfig
+ var err error
+ if !IsDataDir(path) {
+ return dataFolderConfig, nil
+ }
+ dataFolderConfig.Path = path
+ dataFolderConfig.Token, err = GetTokenFromDataDir(path)
+ if err != nil {
+ return dataFolderConfig, err
+ }
+ dataFolderConfig.Network, err = GetNetworkFromDataDir(path)
+ if err != nil {
+ return dataFolderConfig, err
+ }
+
+ dataFolderConfig.Endpoint, _ = GetEndpointFromDataDir(path)
+ dataFolderConfig.PID, _ = GetPidFromDataDir(path)
+
+ return dataFolderConfig, nil
+}
+
+// IsDataDir determines if the specified path is a valid Algorand data directory containing an "algod.token" file.
+func IsDataDir(path string) bool {
+ info, err := os.Stat(path)
+
+ // Check if the path exists
+ if os.IsNotExist(err) {
+ return false
+ }
+
+ // Check if the path is a directory
+ if !info.IsDir() {
+ return false
+ }
+
+ paths := system.FindPathToFile(path, "algod.token")
+ if len(paths) == 1 {
+ return true
+ }
+ return false
+}
+
+// GetKnownDataPaths Does a lazy check for Algorand data directories, based off of known common paths
+func GetKnownDataPaths() []string {
+ home, err := os.UserHomeDir()
+ cobra.CheckErr(err)
+
+ // Hardcoded paths known to be common Algorand data directories
+ commonAlgorandDataDirPaths := []string{
+ "/var/lib/algorand",
+ filepath.Join(home, "node", "data"),
+ filepath.Join(home, ".algorand"),
+ }
+
+ var paths []string
+
+ for _, path := range commonAlgorandDataDirPaths {
+ if IsDataDir(path) {
+ paths = append(paths, path)
+ }
+ }
+
+ return paths
+}
+
+// GetExpiresTime calculates and returns the expiration time of a vote based on rounds and time duration information.
+// If the lastRound and roundTime are not zero, it computes the expiration using round difference and duration.
+// Returns nil if the expiration time cannot be determined.
+func GetExpiresTime(t system.Time, lastRound int, roundTime time.Duration, voteLastValid int) *time.Time {
+ now := t.Now()
+ var expires time.Time
+ if lastRound != 0 &&
+ roundTime != 0 {
+ roundDiff := max(0, voteLastValid-lastRound)
+ distance := int(roundTime) * roundDiff
+ expires = now.Add(time.Duration(distance))
+ return &expires
+ }
+ return nil
+}
+
+func GetTokenFromDataDir(path string) (string, error) {
+ var token string
+
+ file, err := os.ReadFile(path + "/algod.admin.token")
+ if err != nil {
+ return token, err
+ }
+
+ token = strings.Replace(string(file), "\n", "", -1)
+ return token, nil
+}
+
+func GetNetworkFromDataDir(path string) (string, error) {
+ var network string
+ file, err := os.ReadFile(path + "/genesis.json")
+ if err != nil {
+ return network, err
+ }
+ var result map[string]interface{}
+ err = json.Unmarshal(file, &result)
+ if err != nil {
+ return "", err
+ }
+
+ network = fmt.Sprintf("%s-%s", result["network"].(string), result["id"].(string))
+
+ return network, nil
+}
+
+func GetPidFromDataDir(path string) (int, error) {
+ var pid int
+ file, err := os.ReadFile(path + "/algod.pid")
+ if err != nil {
+ return pid, err
+ }
+
+ pid, err = strconv.Atoi(strings.Replace(string(file), "\n", "", -1))
+ if err != nil {
+ return pid, err
+ }
+
+ return pid, nil
+}
+
+func GetEndpointFromDataDir(path string) (string, error) {
+ var endpoint string
+ file, err := os.ReadFile(path + "/algod.net")
+ if err != nil {
+ return AlgodNetEndpointFileMissingAddress, nil
+ }
+
+ endpoint = "http://" + ReplaceEndpointUrl(string(file))
+
+ return endpoint, nil
+}
+
+// ReplaceEndpointUrl replaces newline characters and wildcard IP addresses in a URL with a specific local address.
+func ReplaceEndpointUrl(s string) string {
+ s = strings.Replace(s, "\n", "", -1)
+ s = strings.Replace(s, "0.0.0.0", "127.0.0.1", 1)
+ s = strings.Replace(s, "[::]", "127.0.0.1", 1)
+ return s
+}
diff --git a/internal/algod/version.go b/internal/algod/version.go
new file mode 100644
index 00000000..846b792a
--- /dev/null
+++ b/internal/algod/version.go
@@ -0,0 +1,46 @@
+package algod
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/api"
+)
+
+// VersionResponse represents information about the system version, including network, version, and channel details.
+type VersionResponse struct {
+
+ // Network is a string representing the identifier of the blockchain or network associated with the system.
+ Network string
+
+ // Version is a string representing the version of the system, typically formatted as major.minor.build-channel.
+ Version string
+
+ // Channel is a string representing the release channel of the system, such as stable, beta, or nightly.
+ Channel string
+}
+
+// GetVersion retrieves system version information from the API client and processes it into a formatted VersionResponse.
+func GetVersion(ctx context.Context, client api.ClientWithResponsesInterface) (VersionResponse, api.ResponseInterface, error) {
+ var release VersionResponse
+ v, err := client.GetVersionWithResponse(ctx)
+ if v == nil {
+ return release, v, errors.New(InvalidVersionResponseError)
+ }
+ if err != nil {
+ return release, *v, err
+ }
+ if v.StatusCode() != 200 {
+ return release, v, errors.New(InvalidVersionResponseError)
+ }
+ release.Version = fmt.Sprintf("v%d.%d.%d-%s",
+ v.JSON200.Build.Major,
+ v.JSON200.Build.Minor,
+ v.JSON200.Build.BuildNumber,
+ v.JSON200.Build.Channel,
+ )
+ release.Network = v.JSON200.GenesisId
+ release.Channel = v.JSON200.Build.Channel
+
+ return release, v, nil
+}
diff --git a/internal/block.go b/internal/block.go
deleted file mode 100644
index 8ca8d54e..00000000
--- a/internal/block.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package internal
-
-import (
- "context"
- "errors"
- "github.com/algorandfoundation/algorun-tui/api"
- "time"
-)
-
-type BlockMetrics struct {
- AvgTime time.Duration
- TPS float64
-}
-
-func GetBlockMetrics(ctx context.Context, client api.ClientWithResponsesInterface, round uint64, window int) (*BlockMetrics, error) {
- var avgs = BlockMetrics{
- AvgTime: 0,
- TPS: 0,
- }
- if round < uint64(window) {
- return &avgs, nil
- }
- var format api.GetBlockParamsFormat = "json"
- a, err := client.GetBlockWithResponse(ctx, int(round), &api.GetBlockParams{
- Format: &format,
- })
- if err != nil {
- return nil, err
- }
- if a.StatusCode() != 200 {
- return nil, errors.New(a.Status())
- }
- b, err := client.GetBlockWithResponse(ctx, int(round)-window, &api.GetBlockParams{
- Format: &format,
- })
- if err != nil {
- return nil, err
- }
- if b.StatusCode() != 200 {
- return nil, errors.New(b.Status())
- }
-
- // Push to the transactions count list
- aTimestampRes := a.JSON200.Block["ts"]
- bTimestampRes := b.JSON200.Block["ts"]
- if aTimestampRes == nil || bTimestampRes == nil {
- return &avgs, nil
- }
- aTimestamp := time.Duration(aTimestampRes.(float64)) * time.Second
- bTimestamp := time.Duration(bTimestampRes.(float64)) * time.Second
-
- // Transaction Counter
- aTransactions := a.JSON200.Block["tc"]
- bTransactions := b.JSON200.Block["tc"]
-
- avgs.AvgTime = time.Duration((int(aTimestamp - bTimestamp)) / window)
- if aTransactions != nil && bTransactions != nil {
- avgs.TPS = (aTransactions.(float64) - bTransactions.(float64)) / (float64(window) * avgs.AvgTime.Seconds())
- }
-
- return &avgs, nil
-}
diff --git a/internal/github.go b/internal/github.go
deleted file mode 100644
index 87abc699..00000000
--- a/internal/github.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package internal
-
-import (
- "encoding/json"
- "strings"
-)
-
-func GetGoAlgorandRelease(channel string, http HttpPkgInterface) (*string, error) {
- resp, err := http.Get("https://api.github.com/repos/algorand/go-algorand/releases")
- if err != nil {
- return nil, err
- }
-
- defer resp.Body.Close()
- var versions []map[string]interface{}
- if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil {
- return nil, err
- }
- var versionResponse *string
- for i := range versions {
- tn := versions[i]["tag_name"].(string)
- if strings.Contains(tn, channel) {
- versionResponse = &tn
- break
- }
-
- }
-
- return versionResponse, nil
-}
diff --git a/internal/github_test.go b/internal/github_test.go
deleted file mode 100644
index 8b0b1d40..00000000
--- a/internal/github_test.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package internal
-
-import (
- "bytes"
- "errors"
- "io"
- "net/http"
- "testing"
-)
-
-type testDecoder struct {
- HttpPkgInterface
-}
-
-func (testDecoder) Get(url string) (resp *http.Response, err error) {
- return &http.Response{
- Status: "",
- StatusCode: 0,
- Proto: "",
- ProtoMajor: 0,
- ProtoMinor: 0,
- Header: nil,
- Body: http.NoBody,
- ContentLength: 0,
- TransferEncoding: nil,
- Close: false,
- Uncompressed: false,
- Trailer: nil,
- Request: nil,
- TLS: nil,
- }, nil
-}
-
-type testResponse struct {
- HttpPkgInterface
-}
-
-var jsonStr = `[{
- "tag_name": "v3.26.0-beta"
- }]`
-
-func (testResponse) Get(url string) (resp *http.Response, err error) {
-
- responseBody := io.NopCloser(bytes.NewReader([]byte(jsonStr)))
- return &http.Response{
- StatusCode: 200,
- Body: responseBody,
- }, nil
-}
-
-type testError struct {
- HttpPkgInterface
-}
-
-func (testError) Get(url string) (resp *http.Response, err error) {
- return &http.Response{
- StatusCode: 404,
- }, errors.New("not found")
-}
-
-func Test_Github(t *testing.T) {
- _, err := GetGoAlgorandRelease("beta", new(testDecoder))
- if err == nil {
- t.Error("should fail to decode")
- }
-
- r, err := GetGoAlgorandRelease("beta", new(testResponse))
- if err != nil {
- t.Error(err)
- }
- if r == nil {
- t.Error("should not be nil")
- }
- if *r != "v3.26.0-beta" {
- t.Error("should return v3.26.0-beta")
- }
-
- _, err = GetGoAlgorandRelease("beta", new(testError))
- if err == nil {
- t.Error("should fail to get")
- }
-}
diff --git a/internal/http.go b/internal/http.go
deleted file mode 100644
index 9f2fc57f..00000000
--- a/internal/http.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package internal
-
-import "net/http"
-
-type HttpPkg struct {
- HttpPkgInterface
-}
-
-func (HttpPkg) Get(url string) (resp *http.Response, err error) {
- return http.Get(url)
-}
-
-var Http HttpPkg
-
-type HttpPkgInterface interface {
- Get(url string) (resp *http.Response, err error)
-}
diff --git a/internal/metrics.go b/internal/metrics.go
deleted file mode 100644
index c3bd98b0..00000000
--- a/internal/metrics.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package internal
-
-import (
- "context"
- "errors"
- "github.com/algorandfoundation/algorun-tui/api"
- "regexp"
- "strconv"
- "strings"
- "time"
-)
-
-type MetricsModel struct {
- Enabled bool
- Window int
- RoundTime time.Duration
- TPS float64
- RX int
- TX int
- LastTS time.Time
- LastRX int
- LastTX int
-}
-
-type MetricsResponse map[string]int
-
-func parseMetricsContent(content string) (MetricsResponse, error) {
- var err error
- result := MetricsResponse{}
-
- // Validate the Content
- var isValid bool
- isValid, err = regexp.MatchString(`^#`, content)
- isValid = isValid && err == nil && content != ""
- if !isValid {
- return nil, errors.New("invalid metrics content")
- }
-
- // Regex for Metrics Format,
- // selects all content that does not start with # in multiline mode
- re := regexp.MustCompile(`(?m)^[^#].*`)
- rows := re.FindAllString(content, -1)
-
- // Add the strings to the map
- for _, row := range rows {
- var value int
- keyValue := strings.Split(row, " ")
- value, err = strconv.Atoi(keyValue[1])
- result[keyValue[0]] = value
- }
-
- // Handle any error results
- if err != nil {
- return nil, err
- }
-
- // Give the user what they asked for
- return result, nil
-}
-
-// GetMetrics parses the /metrics endpoint from algod into a map
-func GetMetrics(ctx context.Context, client api.ClientWithResponsesInterface) (MetricsResponse, error) {
- res, err := client.MetricsWithResponse(ctx)
- if err != nil {
- return nil, err
- }
-
- if res.StatusCode() != 200 {
- return nil, errors.New("invalid status code")
- }
-
- return parseMetricsContent(string(res.Body))
-}
diff --git a/internal/participation.go b/internal/participation.go
deleted file mode 100644
index 55b0594d..00000000
--- a/internal/participation.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package internal
-
-import (
- "context"
- "encoding/base64"
- "errors"
- "fmt"
- "net/url"
- "strings"
- "time"
-
- "github.com/algorandfoundation/algorun-tui/api"
-)
-
-// GetPartKeys get the participation keys from the node
-func GetPartKeys(ctx context.Context, client api.ClientWithResponsesInterface) (*[]api.ParticipationKey, error) {
- parts, err := client.GetParticipationKeysWithResponse(ctx)
- if err != nil {
- return nil, err
- }
- if parts.StatusCode() != 200 {
- return nil, errors.New(parts.Status())
- }
- return parts.JSON200, err
-}
-
-// ReadPartKey get a specific participation key by id
-func ReadPartKey(ctx context.Context, client api.ClientWithResponsesInterface, participationId string) (*api.ParticipationKey, error) {
- key, err := client.GetParticipationKeyByIDWithResponse(ctx, participationId)
- if err != nil {
- return nil, err
- }
- if key.StatusCode() != 200 {
- return nil, errors.New(key.Status())
- }
- return key.JSON200, err
-}
-
-// GenerateKeyPair creates a keypair and finds the result
-func GenerateKeyPair(
- ctx context.Context,
- client api.ClientWithResponsesInterface,
- address string,
- params *api.GenerateParticipationKeysParams,
-) (*api.ParticipationKey, error) {
- // Generate a new keypair
- key, err := client.GenerateParticipationKeysWithResponse(ctx, address, params)
- if err != nil {
- return nil, err
- }
- if key.StatusCode() != 200 {
- status := key.Status()
- if status != "" {
- return nil, errors.New(status)
- }
- return nil, errors.New("something went wrong")
- }
- for {
- select {
- case <-ctx.Done():
- return nil, context.Canceled
- case <-time.After(2 * time.Second):
- partKeys, err := GetPartKeys(ctx, client)
- if partKeys == nil || err != nil {
- return nil, errors.New("failed to get participation keys")
- }
- for _, k := range *partKeys {
- if k.Address == address &&
- k.Key.VoteFirstValid == params.First &&
- k.Key.VoteLastValid == params.Last {
- return &k, nil
- }
- }
- case <-time.After(20 * time.Minute):
- return nil, errors.New("timeout waiting for key to be created")
- }
- }
-}
-
-// DeletePartKey remove a key from the node
-func DeletePartKey(ctx context.Context, client api.ClientWithResponsesInterface, participationId string) error {
- deletion, err := client.DeleteParticipationKeyByIDWithResponse(ctx, participationId)
- if err != nil {
- return err
- }
- if deletion.StatusCode() != 200 {
- return errors.New(deletion.Status())
- }
- return nil
-}
-
-// Removes a participation key from the list of keys
-func RemovePartKeyByID(slice *[]api.ParticipationKey, id string) {
- for i, item := range *slice {
- if item.Id == id {
- *slice = append((*slice)[:i], (*slice)[i+1:]...)
- return
- }
- }
-}
-
-func FindParticipationIdForVoteKey(slice *[]api.ParticipationKey, votekey []byte) *string {
- for _, item := range *slice {
- if string(item.Key.VoteParticipationKey) == string(votekey) {
- return &item.Id
- }
- }
- return nil
-}
-
-func ToLoraDeepLink(network string, offline bool, incentiveEligible bool, part api.ParticipationKey) (string, error) {
- var loraNetwork = strings.Replace(strings.Replace(network, "-v1.0", "", 1), "-v1", "", 1)
- if loraNetwork == "dockernet" || loraNetwork == "tuinet" {
- loraNetwork = "localnet"
- }
-
- var query = ""
- encodedIndex := url.QueryEscape("[0]")
- if offline {
- query = fmt.Sprintf(
- "type[0]=keyreg&sender[0]=%s",
- part.Address,
- )
- } else {
- query = fmt.Sprintf(
- "type[0]=keyreg&sender[0]=%s&selkey[0]=%s&sprfkey[0]=%s&votekey[0]=%s&votefst[0]=%d&votelst[0]=%d&votekd[0]=%d",
- part.Address,
- base64.RawURLEncoding.EncodeToString(part.Key.SelectionParticipationKey),
- base64.RawURLEncoding.EncodeToString(*part.Key.StateProofKey),
- base64.RawURLEncoding.EncodeToString(part.Key.VoteParticipationKey),
- part.Key.VoteFirstValid,
- part.Key.VoteLastValid,
- part.Key.VoteKeyDilution,
- )
- if incentiveEligible {
- // TODO: enable fee with either feature flag or config flag
- // query += fmt.Sprintf("&fee[0]=%d", 2000000)
- }
- }
- return fmt.Sprintf("https://lora.algokit.io/%s/transaction-wizard?%s", loraNetwork, strings.Replace(query, "[0]", encodedIndex, -1)), nil
-}
diff --git a/internal/participation_test.go b/internal/participation_test.go
deleted file mode 100644
index 55bac2d0..00000000
--- a/internal/participation_test.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package internal
-
-import (
- "context"
- "fmt"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal/test"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
- "testing"
-)
-
-func Test_ToLoraDeeplink(t *testing.T) {
- link, err := ToLoraDeepLink("tuinet-v1", true, true, api.ParticipationKey{
- Address: "ABC",
- EffectiveFirstValid: nil,
- EffectiveLastValid: nil,
- Id: "",
- Key: api.AccountParticipation{},
- LastBlockProposal: nil,
- LastStateProof: nil,
- LastVote: nil,
- })
- if err != nil {
- t.Error(err)
- }
- if link != "https://lora.algokit.io/localnet/transaction-wizard?type%5B0%5D=keyreg&sender%5B0%5D=ABC" {
- t.Error("Link should be a known deeplink")
- }
-
- // TODO put back
- // link, err = ToLoraDeepLink("tuinet-v1", false, true, mock.Keys[0])
- // if err != nil {
- // t.Error(err)
- // }
- // if link != "https://lora.algokit.io/localnet/transaction-wizard?type%5B0%5D=keyreg&sender%5B0%5D=ABC&selkey%5B0%5D=VEVTVEtFWQ&sprfkey%5B0%5D=VEVTVEtFWQ&votekey%5B0%5D=VEVTVEtFWQ&votefst%5B0%5D=0&votelst%5B0%5D=30000&votekd%5B0%5D=100&fee%5B0%5D=2000000" {
- // t.Error("Link should be a known deeplink fee")
- // }
-
- link, err = ToLoraDeepLink("tuinet-v1", false, false, mock.Keys[0])
- if err != nil {
- t.Error(err)
- }
- if link != "https://lora.algokit.io/localnet/transaction-wizard?type%5B0%5D=keyreg&sender%5B0%5D=ABC&selkey%5B0%5D=VEVTVEtFWQ&sprfkey%5B0%5D=VEVTVEtFWQ&votekey%5B0%5D=VEVTVEtFWQ&votefst%5B0%5D=0&votelst%5B0%5D=30000&votekd%5B0%5D=100" {
- t.Error("Link should be a known deeplink fee")
- }
-
-}
-
-func Test_ListParticipationKeys(t *testing.T) {
- ctx := context.Background()
- client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
- if err != nil {
- t.Fatal(err)
- }
-
- _, err = GetPartKeys(ctx, client)
-
- // Expect unauthorized for Urtho servers
- if err == nil {
- t.Fatal(err)
- }
-
- // Setup elevated client
- tClient := test.GetClient(false)
-
- keys, err := GetPartKeys(ctx, tClient)
- if err != nil {
- t.Fatal(err)
- }
-
- fmt.Println(keys)
-}
-
-func Test_ReadParticipationKey(t *testing.T) {
- ctx := context.Background()
- client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
- if err != nil {
- t.Fatal(err)
- }
-
- _, err = ReadPartKey(ctx, client, "unknown")
-
- // Expect unauthorized for Urtho servers
- if err == nil {
- t.Fatal(err)
- }
-
- tClient := test.GetClient(false)
-
- keys, err := GetPartKeys(ctx, tClient)
- if err != nil {
- t.Fatal(err)
- }
- if keys == nil {
- t.Fatal(err)
- }
-
- _, err = ReadPartKey(ctx, tClient, (*keys)[0].Id)
-
- if err != nil {
- t.Fatal(err)
- }
-
-}
-
-func Test_GenerateParticipationKey(t *testing.T) {
- ctx := context.Background()
-
- // Create Client
- client, err := api.NewClientWithResponses("https://mainnet-api.4160.nodely.dev:443")
- if err != nil {
- t.Fatal(err)
- }
- // Generate error
- _, err = GenerateKeyPair(ctx, client, "", nil)
- if err == nil {
- t.Fatal(err)
- }
-
- // Setup test client
- tClient := test.GetClient(false)
-
- params := api.GenerateParticipationKeysParams{
- Dilution: nil,
- First: 0,
- Last: 30,
- }
-
- // This returns nothing and sucks
- key, err := GenerateKeyPair(ctx, tClient, "ABC", ¶ms)
- if err != nil {
- t.Fatal(err)
- }
- fmt.Println(key)
-}
-
-func Test_DeleteParticipationKey(t *testing.T) {
- ctx := context.Background()
-
- client := test.GetClient(false)
- params := api.GenerateParticipationKeysParams{
- Dilution: nil,
- First: 0,
- Last: 30000,
- }
- key, err := GenerateKeyPair(ctx, client, "ABC", ¶ms)
- if err != nil {
- t.Fatal(err)
- }
-
- err = DeletePartKey(ctx, client, key.Id)
- if err != nil {
- t.Fatal(err)
- }
-}
-func Test_RemovePartKeyByID(t *testing.T) {
- // Test case: Remove an existing key
- t.Run("Remove existing key", func(t *testing.T) {
- keys := []api.ParticipationKey{
- {Id: "key1"},
- {Id: "key2"},
- {Id: "key3"},
- }
- expectedKeys := []api.ParticipationKey{
- {Id: "key1"},
- {Id: "key3"},
- }
- RemovePartKeyByID(&keys, "key2")
- if len(keys) != len(expectedKeys) {
- t.Fatalf("expected %d keys, got %d", len(expectedKeys), len(keys))
- }
- for i, key := range keys {
- if key.Id != expectedKeys[i].Id {
- t.Fatalf("expected key ID %s, got %s", expectedKeys[i].Id, key.Id)
- }
- }
- })
-
- // Test case: Remove a non-existing key
- t.Run("Remove non-existing key", func(t *testing.T) {
- keys := []api.ParticipationKey{
- {Id: "key1"},
- {Id: "key2"},
- {Id: "key3"},
- }
- expectedKeys := []api.ParticipationKey{
- {Id: "key1"},
- {Id: "key2"},
- {Id: "key3"},
- }
- RemovePartKeyByID(&keys, "key4")
- if len(keys) != len(expectedKeys) {
- t.Fatalf("expected %d keys, got %d", len(expectedKeys), len(keys))
- }
- for i, key := range keys {
- if key.Id != expectedKeys[i].Id {
- t.Fatalf("expected key ID %s, got %s", expectedKeys[i].Id, key.Id)
- }
- }
- })
-
- // Test case: Remove a key from an empty list
- t.Run("Remove key from empty list", func(t *testing.T) {
- keys := []api.ParticipationKey{}
- RemovePartKeyByID(&keys, "key1")
- if len(keys) != 0 {
- t.Fatalf("expected 0 keys, got %d", len(keys))
- }
- })
-}
diff --git a/internal/rangetype.go b/internal/rangetype.go
deleted file mode 100644
index 10e8497f..00000000
--- a/internal/rangetype.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package internal
-
-type RangeType string
-
-const (
- TimeRange RangeType = "seconds"
- RoundRange RangeType = "rounds"
-)
diff --git a/internal/state.go b/internal/state.go
deleted file mode 100644
index f72c76ef..00000000
--- a/internal/state.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package internal
-
-import (
- "context"
- "errors"
- "time"
-
- "github.com/algorandfoundation/algorun-tui/api"
-)
-
-type StateModel struct {
- // Models
- Status StatusModel
- Metrics MetricsModel
- Accounts map[string]Account
- ParticipationKeys *[]api.ParticipationKey
-
- // Application State
- Admin bool
-
- // TODO: handle contexts instead of adding it to state
- Watching bool
-
- // RPC
- Client api.ClientWithResponsesInterface
- Context context.Context
-}
-
-func (s *StateModel) waitAfterError(err error, cb func(model *StateModel, err error)) {
- if err != nil {
- s.Status.State = "DOWN"
- cb(nil, err)
- time.Sleep(time.Second * 3)
- }
-}
-
-// TODO: allow context to handle loop
-func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Context, client api.ClientWithResponsesInterface) {
- s.Watching = true
- if s.Metrics.Window == 0 {
- s.Metrics.Window = 100
- }
-
- err := s.Status.Fetch(ctx, client, new(HttpPkg))
- if err != nil {
- cb(nil, err)
- }
-
- lastRound := s.Status.LastRound
-
- for {
- if !s.Watching {
- break
- }
-
- if s.Status.State == FastCatchupState {
- time.Sleep(time.Second * 10)
- err := s.Status.Fetch(ctx, client, new(HttpPkg))
- if err != nil {
- cb(nil, err)
- }
- continue
- }
-
- status, err := client.WaitForBlockWithResponse(ctx, int(lastRound))
- s.waitAfterError(err, cb)
- if err != nil {
- continue
- }
- if status.StatusCode() != 200 {
- s.waitAfterError(errors.New(status.Status()), cb)
- continue
- }
-
- s.Status.State = "Unknown"
-
- // Update Status
- s.Status.Update(status.JSON200.LastRound, status.JSON200.CatchupTime, status.JSON200.Catchpoint, status.JSON200.UpgradeNodeVote)
-
- // Fetch Keys
- s.UpdateKeys()
-
- if s.Status.State == SyncingState {
- lastRound = s.Status.LastRound
- cb(s, nil)
- continue
- }
- // Run Round Averages and RX/TX every 5 rounds
- if s.Status.LastRound%5 == 0 || (s.Status.LastRound > 100 && s.Metrics.RoundTime.Seconds() == 0) {
- bm, err := GetBlockMetrics(ctx, client, s.Status.LastRound, s.Metrics.Window)
- s.waitAfterError(err, cb)
- if err != nil {
- continue
- }
- s.Metrics.RoundTime = bm.AvgTime
- s.Metrics.TPS = bm.TPS
- s.UpdateMetricsFromRPC(ctx, client)
- }
-
- lastRound = s.Status.LastRound
- cb(s, nil)
- }
-}
-
-func (s *StateModel) Stop() {
- s.Watching = false
-}
-
-func (s *StateModel) UpdateMetricsFromRPC(ctx context.Context, client api.ClientWithResponsesInterface) {
- // Fetch RX/TX
- res, err := GetMetrics(ctx, client)
- if err != nil {
- s.Metrics.Enabled = false
- }
- if err == nil {
- s.Metrics.Enabled = true
- now := time.Now()
- diff := now.Sub(s.Metrics.LastTS)
-
- s.Metrics.TX = max(0, int(float64(res["algod_network_sent_bytes_total"]-s.Metrics.LastTX)/diff.Seconds()))
- s.Metrics.RX = max(0, int(float64(res["algod_network_received_bytes_total"]-s.Metrics.LastRX)/diff.Seconds()))
-
- s.Metrics.LastTS = now
- s.Metrics.LastTX = res["algod_network_sent_bytes_total"]
- s.Metrics.LastRX = res["algod_network_received_bytes_total"]
- }
-}
-func (s *StateModel) UpdateAccounts() error {
- var err error
- s.Accounts, err = AccountsFromState(s, new(Clock), s.Client)
- return err
-}
-
-func (s *StateModel) UpdateKeys() {
- var err error
- s.ParticipationKeys, err = GetPartKeys(s.Context, s.Client)
- if err != nil {
- s.Admin = false
- }
- if err == nil {
- s.Admin = true
- err = s.UpdateAccounts()
- if err != nil {
- // TODO: Handle error
- }
- }
-}
diff --git a/internal/status.go b/internal/status.go
deleted file mode 100644
index 9425b015..00000000
--- a/internal/status.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package internal
-
-import (
- "context"
- "fmt"
-
- "github.com/algorandfoundation/algorun-tui/api"
-)
-
-type State string
-
-const (
- FastCatchupState State = "FAST-CATCHUP"
- SyncingState State = "SYNCING"
- StableState State = "RUNNING"
-)
-
-// StatusModel represents a status response from algod.Status
-type StatusModel struct {
- State State
- Version string
- Network string
- Voting bool
- NeedsUpdate bool
- LastRound uint64 // Last recorded round
-}
-
-// String prints the last round value
-func (m *StatusModel) String() string {
- return fmt.Sprintf("\nLastRound: %d\n", m.LastRound)
-}
-func (m *StatusModel) Update(lastRound int, catchupTime int, catchpoint *string, upgradeNodeVote *bool) {
- m.LastRound = uint64(lastRound)
- if catchupTime > 0 {
- if catchpoint != nil && *catchpoint != "" {
- m.State = FastCatchupState
- } else {
- m.State = SyncingState
- }
- } else {
- m.State = StableState
- }
- if upgradeNodeVote != nil {
- m.Voting = *upgradeNodeVote
- }
-}
-
-// Fetch handles algod.Status
-func (m *StatusModel) Fetch(ctx context.Context, client api.ClientWithResponsesInterface, httpPkg HttpPkgInterface) error {
- if m.Version == "" || m.Version == "N/A" {
- v, err := client.GetVersionWithResponse(ctx)
- if err != nil {
- return err
- }
- if v.StatusCode() != 200 {
- return fmt.Errorf("Status code %d: %s", v.StatusCode(), v.Status())
- }
- m.Network = v.JSON200.GenesisId
- m.Version = fmt.Sprintf("v%d.%d.%d-%s", v.JSON200.Build.Major, v.JSON200.Build.Minor, v.JSON200.Build.BuildNumber, v.JSON200.Build.Channel)
- currentRelease, err := GetGoAlgorandRelease(v.JSON200.Build.Channel, httpPkg)
- if err != nil {
- return err
- }
- if currentRelease != nil && m.Version != *currentRelease {
- m.NeedsUpdate = true
- } else {
- m.NeedsUpdate = false
- }
- }
-
- s, err := client.GetStatusWithResponse(ctx)
- if err != nil {
- return err
- }
-
- if s.StatusCode() != 200 {
- return fmt.Errorf("Status code %d: %s", s.StatusCode(), s.Status())
- }
-
- m.Update(s.JSON200.LastRound, s.JSON200.CatchupTime, s.JSON200.Catchpoint, s.JSON200.UpgradeNodeVote)
- return nil
-}
diff --git a/internal/status_test.go b/internal/status_test.go
deleted file mode 100644
index e8d969bd..00000000
--- a/internal/status_test.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package internal
-
-import (
- "context"
- "github.com/algorandfoundation/algorun-tui/internal/test"
- "strings"
- "testing"
-)
-
-func Test_StatusModel(t *testing.T) {
- m := StatusModel{LastRound: 0}
- if !strings.Contains(m.String(), "LastRound: 0") {
- t.Fatal("expected \"LastRound: 0\", got ", m.String())
- }
-
- stale := true
- m.Update(5, 10, nil, &stale)
-
- if m.LastRound != 5 {
- t.Errorf("expected LastRound: 5, got %d", m.LastRound)
- }
- if m.State != SyncingState {
- t.Errorf("expected State: %s, got %s", SyncingState, m.State)
- }
-
- m.Update(10, 0, nil, &stale)
- if m.LastRound != 10 {
- t.Errorf("expected LastRound: 10, got %d", m.LastRound)
- }
- if m.State != StableState {
- t.Errorf("expected State: %s, got %s", StableState, m.State)
- }
-
-}
-
-func Test_StatusFetch(t *testing.T) {
- client := test.GetClient(true)
- m := StatusModel{LastRound: 0}
- pkg := new(HttpPkg)
- err := m.Fetch(context.Background(), client, pkg)
- if err == nil {
- t.Error("expected error, got nil")
- }
-
- client = test.NewClient(false, true)
- err = m.Fetch(context.Background(), client, pkg)
- if err == nil {
- t.Error("expected error, got nil")
- }
-
- client = test.GetClient(false)
- err = m.Fetch(context.Background(), client, pkg)
- if err != nil {
- t.Error(err)
- }
- if m.LastRound == 0 {
- t.Error("expected LastRound to be non-zero")
- }
-
-}
diff --git a/internal/system/cmds.go b/internal/system/cmds.go
new file mode 100644
index 00000000..e021cadd
--- /dev/null
+++ b/internal/system/cmds.go
@@ -0,0 +1,124 @@
+package system
+
+import (
+ "fmt"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "sync"
+)
+
+// CmdFailedErrorMsg is a formatted error message used to detail command failures, including output and the associated error.
+const CmdFailedErrorMsg = "command failed: %s output: %s error: %v"
+
+// IsSudo checks if the process is running with root privileges by verifying the effective user ID is 0.
+func IsSudo() bool {
+ return os.Geteuid() == 0
+}
+
+// IsCmdRunning checks if a command with the specified name is currently running using the `pgrep` command.
+// Returns true if the command is running, otherwise false.
+func IsCmdRunning(name string) bool {
+ err := exec.Command("pgrep", name).Run()
+ return err == nil
+}
+
+// CmdExists checks that a bash cli/cmd tool exists
+func CmdExists(tool string) bool {
+ _, err := exec.LookPath(tool)
+ return err == nil
+}
+
+// CmdsList represents a list of command sequences where each command is defined as a slice of strings.
+type CmdsList [][]string
+
+// Su updates each command in the CmdsList to prepend "sudo -u " unless it already starts with "sudo".
+func (l CmdsList) Su(user string) CmdsList {
+ for i, args := range l {
+ if !strings.HasPrefix(args[0], "sudo") {
+ l[i] = append([]string{"sudo", "-u", user}, args...)
+ }
+ }
+ return l
+}
+
+// Run executes a command with the given arguments and returns its combined output and any resulting error.
+func Run(args []string) (string, error) {
+ cmd := exec.Command(args[0], args[1:]...)
+ output, err := cmd.CombinedOutput()
+ return string(output), err
+}
+
+// RunAll executes each command in the CmdsList sequentially, logging errors or debug messages for each command execution.
+// Returns an error if any command fails, including the command details, output, and error message.
+func RunAll(list CmdsList) error {
+ // Run each installation command
+ for _, args := range list {
+ cmd := exec.Command(args[0], args[1:]...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ log.Error(fmt.Sprintf("%s: %s", style.Red.Render("Failed"), strings.Join(args, " ")))
+ return fmt.Errorf(CmdFailedErrorMsg, strings.Join(args, " "), output, err)
+ }
+ log.Debug(fmt.Sprintf("%s: %s", style.Green.Render("Running"), strings.Join(args, " ")))
+ }
+ return nil
+}
+
+// FindPathToFile finds path(s) to a file in a directory and its subdirectories using parallel processing
+func FindPathToFile(startDir string, targetFileName string) []string {
+ var dirPaths []string
+ var mu sync.Mutex
+ var wg sync.WaitGroup
+
+ fileChan := make(chan string)
+
+ // Worker function to process files
+ worker := func() {
+ defer wg.Done()
+ for path := range fileChan {
+ info, err := os.Stat(path)
+ if err != nil {
+ continue
+ }
+ if !info.IsDir() && info.Name() == targetFileName {
+ dirPath := filepath.Dir(path)
+ mu.Lock()
+ dirPaths = append(dirPaths, dirPath)
+ mu.Unlock()
+ }
+ }
+ }
+
+ // Start worker goroutines
+ numWorkers := 4 // Adjust the number of workers based on your system's capabilities
+ for i := 0; i < numWorkers; i++ {
+ wg.Add(1)
+ go worker()
+ }
+
+ // Walk the directory tree and send file paths to the channel
+ err := filepath.Walk(startDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ // Ignore permission msgs
+ if os.IsPermission(err) {
+ return nil
+ }
+ return err
+ }
+ fileChan <- path
+ return nil
+ })
+
+ close(fileChan)
+ wg.Wait()
+
+ if err != nil {
+ panic(err)
+ }
+
+ return dirPaths
+}
diff --git a/internal/system/service.go b/internal/system/service.go
new file mode 100644
index 00000000..830097ae
--- /dev/null
+++ b/internal/system/service.go
@@ -0,0 +1,17 @@
+package system
+
+// Interface defines methods for managing and interacting with a system service.
+type Interface interface {
+ IsInstalled() bool
+ IsRunning() bool
+ IsService() bool
+ SetNetwork(network string) error
+ Install() error
+ Update() error
+ Uninstall() error
+ Start() error
+ Stop() error
+ Restart() error
+ UpdateService(dataDirectoryPath string) error
+ EnsureService() error
+}
diff --git a/internal/system/time.go b/internal/system/time.go
new file mode 100644
index 00000000..1e2ad52b
--- /dev/null
+++ b/internal/system/time.go
@@ -0,0 +1,14 @@
+package system
+
+import "time"
+
+// Time provides an interface for retrieving the current time.
+type Time interface {
+ Now() time.Time
+}
+
+// Clock is a struct representing a mechanism to retrieve the current time.
+type Clock struct{}
+
+// Now retrieves the current local time as a time.Time instance.
+func (Clock) Now() time.Time { return time.Now() }
diff --git a/internal/test/client.go b/internal/test/client.go
index c6330f75..14161006 100644
--- a/internal/test/client.go
+++ b/internal/test/client.go
@@ -3,21 +3,30 @@ package test
import (
"context"
"errors"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
"net/http"
)
+// GetClient creates and returns an implementation of api.ClientWithResponsesInterface, determining behavior based on throws.
+// If throws is true, the resulting client may simulate error scenarios for testing purposes.
func GetClient(throws bool) api.ClientWithResponsesInterface {
return NewClient(throws, false)
}
+// Client represents a client that implements the api.ClientWithResponsesInterface for performing API operations.
+// Errors indicates whether the client should simulate error responses during API calls.
+// Invalid indicates whether the client should simulate invalid responses during API calls.
type Client struct {
api.ClientWithResponsesInterface
Errors bool
Invalid bool
}
+// NewClient initializes and returns an instance of api.ClientWithResponsesInterface.
+// The behavior of the client can be configured with the throws and invalid parameters.
+// If throws is true, the client is set to simulate error responses.
+// If invalid is true, the client is set to simulate invalid responses.
func NewClient(throws bool, invalid bool) api.ClientWithResponsesInterface {
client := new(Client)
if throws {
@@ -29,6 +38,7 @@ func NewClient(throws bool, invalid bool) api.ClientWithResponsesInterface {
return client
}
+// MetricsWithResponse fetches metrics data from the server and returns the response and any error encountered.
func (c *Client) MetricsWithResponse(ctx context.Context, reqEditors ...api.RequestEditorFn) (*api.MetricsResponse, error) {
var res api.MetricsResponse
body := `# HELP algod_telemetry_drops_total telemetry messages dropped due to full queues
@@ -67,6 +77,8 @@ algod_crypto_vrf_hash_total 0`
}
return &res, nil
}
+
+// GetParticipationKeyByIDWithResponse fetches a participation key by its ID and returns the response or an error.
func (c *Client) GetParticipationKeyByIDWithResponse(ctx context.Context, participationId string, reqEditors ...api.RequestEditorFn) (*api.GetParticipationKeyByIDResponse, error) {
var res api.GetParticipationKeyByIDResponse
if !c.Invalid {
@@ -99,13 +111,17 @@ func (c *Client) GetParticipationKeyByIDWithResponse(ctx context.Context, partic
}
func (c *Client) GetParticipationKeysWithResponse(ctx context.Context, reqEditors ...api.RequestEditorFn) (*api.GetParticipationKeysResponse, error) {
var res api.GetParticipationKeysResponse
- clone := mock.Keys
+ clone := &mock.Keys
+ keys := make([]api.ParticipationKey, len(*clone))
+ for k := range *clone {
+ keys = append(keys, (*clone)[k])
+ }
if !c.Invalid {
httpResponse := http.Response{StatusCode: 200}
res = api.GetParticipationKeysResponse{
Body: nil,
HTTPResponse: &httpResponse,
- JSON200: &clone,
+ JSON200: &keys,
JSON400: nil,
JSON401: nil,
JSON404: nil,
@@ -116,7 +132,7 @@ func (c *Client) GetParticipationKeysWithResponse(ctx context.Context, reqEditor
res = api.GetParticipationKeysResponse{
Body: nil,
HTTPResponse: &httpResponse,
- JSON200: &clone,
+ JSON200: &keys,
JSON400: nil,
JSON401: nil,
JSON404: nil,
diff --git a/internal/test/mock/fixtures.go b/internal/test/mock/fixtures.go
index b3f91de8..59293229 100644
--- a/internal/test/mock/fixtures.go
+++ b/internal/test/mock/fixtures.go
@@ -1,7 +1,7 @@
package mock
import (
- "github.com/algorandfoundation/algorun-tui/api"
+ "github.com/algorandfoundation/nodekit/api"
)
var VoteKey = []byte("TESTKEY")
diff --git a/internal/test/utils.go b/internal/test/utils.go
index 5827d746..af8a336b 100644
--- a/internal/test/utils.go
+++ b/internal/test/utils.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/algorandfoundation/algorun-tui/api"
+ "github.com/algorandfoundation/nodekit/api"
"io"
)
diff --git a/internal/time.go b/internal/time.go
deleted file mode 100644
index eb3fa8fb..00000000
--- a/internal/time.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package internal
-
-import "time"
-
-type Time interface {
- Now() time.Time
-}
-
-type Clock struct{}
-
-func (Clock) Now() time.Time { return time.Now() }
diff --git a/main.go b/main.go
index f282015a..a1ab5450 100644
--- a/main.go
+++ b/main.go
@@ -1,10 +1,27 @@
package main
import (
- "github.com/algorandfoundation/algorun-tui/cmd"
+ "github.com/algorandfoundation/nodekit/cmd"
+ "github.com/charmbracelet/log"
+ "os"
+ "runtime"
)
+func init() {
+ // TODO: handle log files
+ // Log as JSON instead of the default ASCII formatter.
+ //log.SetFormatter(log.JSONFormatter)
+
+ // Output to stdout instead of the default stderr
+ // Can be any io.Writer, see below for File example
+ log.SetOutput(os.Stdout)
+
+ // Only log the warning severity or above.
+ log.SetLevel(log.DebugLevel)
+}
func main() {
+ // TODO: more performance tuning
+ runtime.GOMAXPROCS(1)
err := cmd.Execute()
if err != nil {
return
diff --git a/main_test.go b/main_test.go
deleted file mode 100644
index 207c8325..00000000
--- a/main_test.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package main
-
-import (
- "github.com/spf13/viper"
- "testing"
-)
-
-func Test_Main(t *testing.T) {
- viper.Set("algod-token", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
- viper.Set("algod-endpoint", "http://localhost:8080")
- main()
-}
diff --git a/man/nodekit_bootstrap.md b/man/nodekit_bootstrap.md
new file mode 100644
index 00000000..111cb2ea
--- /dev/null
+++ b/man/nodekit_bootstrap.md
@@ -0,0 +1,33 @@
+## nodekit bootstrap
+
+Initialize a fresh node
+
+### Synopsis
+
+
+
+
+
+Initialize a fresh node
+
+Overview:
+Get up and running with a fresh Algorand node.
+Uses the local package manager to install Algorand, and then starts the node and preforms a Fast-Catchup.
+
+Note: This command only supports the default data directory, /var/lib/algorand
+
+```
+nodekit bootstrap [flags]
+```
+
+### Options
+
+```
+ -h, --help help for bootstrap
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_catchup.md b/man/nodekit_catchup.md
new file mode 100644
index 00000000..c35387b9
--- /dev/null
+++ b/man/nodekit_catchup.md
@@ -0,0 +1,37 @@
+## nodekit catchup
+
+Manage Fast-Catchup for your node
+
+### Synopsis
+
+
+
+
+
+Fast-Catchup is a feature that allows your node to catch up to the network faster than normal.
+
+Overview:
+The entire process should sync a node in minutes rather than hours or days.
+Actual sync times may vary depending on the number of accounts, number of blocks and the network.
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for catchup
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+* [nodekit catchup debug](/man/nodekit_catchup_debug.md) - Display debug information for Fast-Catchup.
+* [nodekit catchup start](/man/nodekit_catchup_start.md) - Get the latest catchpoint and start catching up.
+* [nodekit catchup stop](/man/nodekit_catchup_stop.md) - Stop a fast catchup
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_catchup_debug.md b/man/nodekit_catchup_debug.md
new file mode 100644
index 00000000..b66d6dc3
--- /dev/null
+++ b/man/nodekit_catchup_debug.md
@@ -0,0 +1,33 @@
+## nodekit catchup debug
+
+Display debug information for Fast-Catchup.
+
+### Synopsis
+
+
+
+
+
+Display debug information for Fast-Catchup.
+
+Overview:
+This information is useful for debugging fast-catchup issues.
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup debug [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for debug
+```
+
+### SEE ALSO
+
+* [nodekit catchup](/man/nodekit_catchup.md) - Manage Fast-Catchup for your node
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_catchup_start.md b/man/nodekit_catchup_start.md
new file mode 100644
index 00000000..92306447
--- /dev/null
+++ b/man/nodekit_catchup_start.md
@@ -0,0 +1,34 @@
+## nodekit catchup start
+
+Get the latest catchpoint and start catching up.
+
+### Synopsis
+
+
+
+
+
+Catchup the node to the latest catchpoint.
+
+Overview:
+Starting a catchup will sync the node to the latest catchpoint.
+Actual sync times may vary depending on the number of accounts, number of blocks and the network.
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup start [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for start
+```
+
+### SEE ALSO
+
+* [nodekit catchup](/man/nodekit_catchup.md) - Manage Fast-Catchup for your node
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_catchup_stop.md b/man/nodekit_catchup_stop.md
new file mode 100644
index 00000000..249da1d0
--- /dev/null
+++ b/man/nodekit_catchup_stop.md
@@ -0,0 +1,33 @@
+## nodekit catchup stop
+
+Stop a fast catchup
+
+### Synopsis
+
+
+
+
+
+Stop a fast catchup
+
+Overview:
+Stop an active Fast-Catchup. This will abort the catchup process if one has started
+
+Note: Not all networks support Fast-Catchup.
+
+```
+nodekit catchup stop [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for stop
+```
+
+### SEE ALSO
+
+* [nodekit catchup](/man/nodekit_catchup.md) - Manage Fast-Catchup for your node
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_configure.md b/man/nodekit_configure.md
new file mode 100644
index 00000000..6782d6f0
--- /dev/null
+++ b/man/nodekit_configure.md
@@ -0,0 +1,29 @@
+## nodekit configure
+
+Change settings on the system (WIP)
+
+### Synopsis
+
+
+
+
+
+Change settings on the system (WIP)
+
+Overview:
+Tool for inspecting and updating the Algorand daemon's config.json and service files
+
+Note: This is still a work in progress. Expect bugs and rough edges.
+
+### Options
+
+```
+ -h, --help help for configure
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+* [nodekit configure service](/man/nodekit_configure_service.md) - Install service files for the Algorand daemon.
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_configure_service.md b/man/nodekit_configure_service.md
new file mode 100644
index 00000000..d2f555ab
--- /dev/null
+++ b/man/nodekit_configure_service.md
@@ -0,0 +1,32 @@
+## nodekit configure service
+
+Install service files for the Algorand daemon.
+
+### Synopsis
+
+
+
+
+
+Install service files for the Algorand daemon.
+
+Overview:
+Ensuring that the Algorand daemon is installed and running as a service.
+
+Note: This is still a work in progress. Expect bugs and rough edges.
+
+```
+nodekit configure service [flags]
+```
+
+### Options
+
+```
+ -h, --help help for service
+```
+
+### SEE ALSO
+
+* [nodekit configure](/man/nodekit_configure.md) - Change settings on the system (WIP)
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_debug.md b/man/nodekit_debug.md
new file mode 100644
index 00000000..181b19a9
--- /dev/null
+++ b/man/nodekit_debug.md
@@ -0,0 +1,33 @@
+## nodekit debug
+
+Display debugging information
+
+### Synopsis
+
+
+
+
+
+Display debugging information
+
+Overview:
+Prints the known state of the nodekit
+Checks various paths and configurations to present useful information for bug reports.
+
+
+```
+nodekit debug [flags]
+```
+
+### Options
+
+```
+ -d, --datadir string Data directory for the node
+ -h, --help help for debug
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_install.md b/man/nodekit_install.md
new file mode 100644
index 00000000..cbaeff6b
--- /dev/null
+++ b/man/nodekit_install.md
@@ -0,0 +1,32 @@
+## nodekit install
+
+Install the node daemon
+
+### Synopsis
+
+
+
+
+
+Install the node daemon
+
+Overview:
+Configures the local package manager and installs the algorand daemon on your local machine
+
+
+```
+nodekit install [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully install the node
+ -h, --help help for install
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_start.md b/man/nodekit_start.md
new file mode 100644
index 00000000..9503a83a
--- /dev/null
+++ b/man/nodekit_start.md
@@ -0,0 +1,33 @@
+## nodekit start
+
+Start the node daemon
+
+### Synopsis
+
+
+
+
+
+Start the node daemon
+
+Overview:
+Start the Algorand daemon on your local machine if it is not already running. Optionally, the daemon can be forcefully started.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit start [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully start the node
+ -h, --help help for start
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_stop.md b/man/nodekit_stop.md
new file mode 100644
index 00000000..39dc9efd
--- /dev/null
+++ b/man/nodekit_stop.md
@@ -0,0 +1,33 @@
+## nodekit stop
+
+Stop the node daemon
+
+### Synopsis
+
+
+
+
+
+Stop the node daemon
+
+Overview:
+Stops the Algorand daemon on your local machine. Optionally, the daemon can be forcefully stopped.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit stop [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully stop the node
+ -h, --help help for stop
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_uninstall.md b/man/nodekit_uninstall.md
new file mode 100644
index 00000000..89134aa7
--- /dev/null
+++ b/man/nodekit_uninstall.md
@@ -0,0 +1,33 @@
+## nodekit uninstall
+
+Uninstall the node daemon
+
+### Synopsis
+
+
+
+
+
+Uninstall the node daemon
+
+Overview:
+Uninstall Algorand node (Algod) and other binaries on your system installed by this tool.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit uninstall [flags]
+```
+
+### Options
+
+```
+ -f, --force forcefully uninstall the node
+ -h, --help help for uninstall
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/man/nodekit_upgrade.md b/man/nodekit_upgrade.md
new file mode 100644
index 00000000..1de31a52
--- /dev/null
+++ b/man/nodekit_upgrade.md
@@ -0,0 +1,32 @@
+## nodekit upgrade
+
+Upgrade the node daemon
+
+### Synopsis
+
+
+
+
+
+Upgrade the node daemon
+
+Overview:
+Upgrade Algorand packages if it was installed with package manager.
+
+This requires the daemon to be installed on your system.
+
+```
+nodekit upgrade [flags]
+```
+
+### Options
+
+```
+ -h, --help help for upgrade
+```
+
+### SEE ALSO
+
+* [nodekit](/README.md) - Manage Algorand nodes from the command line
+
+###### Auto generated by spf13/cobra on 7-Jan-2025
diff --git a/playbook.yaml b/playbook.yaml
new file mode 100644
index 00000000..1cf0743a
--- /dev/null
+++ b/playbook.yaml
@@ -0,0 +1,22 @@
+- name: Test Instance
+ hosts: localhost
+ tasks:
+ - name: Ensure nodekit exists
+ stat:
+ path: /usr/bin/nodekit
+ register: binpath
+ - name: Fail missing binary
+ fail:
+ msg: "Must have nodekit installed!"
+ when: not binpath.stat.exists
+ - name: Run installer
+ command: nodekit install
+ - name: Run stop
+ command: nodekit stop
+ - name: Run upgrade
+ command: nodekit upgrade
+ - name: Run stop
+ command: nodekit stop
+ - name: Run Start
+ command: nodekit start
+ # TODO: start a private network, fund TUI account and run TUI integration
\ No newline at end of file
diff --git a/scripts/documentation.go b/scripts/documentation.go
new file mode 100644
index 00000000..2b4121df
--- /dev/null
+++ b/scripts/documentation.go
@@ -0,0 +1,212 @@
+package main
+
+import (
+ "fmt"
+ "github.com/algorandfoundation/nodekit/cmd"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/spf13/cobra/doc"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+)
+
+func copyFile(src, dst string, move bool) error {
+ // Open the source file
+ sourceFile, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer sourceFile.Close()
+
+ // Create the destination file
+ destinationFile, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer destinationFile.Close()
+
+ // Copy the contents from source to destination
+ _, err = io.Copy(destinationFile, sourceFile)
+ if err != nil {
+ return err
+ }
+
+ // Optionally, sync to ensure writes are flushed to disk
+ err = destinationFile.Sync()
+ if err != nil {
+ return err
+ }
+ if !move {
+ return nil
+ }
+
+ return os.Remove(src)
+}
+func appendString(filePath, content string) error {
+ // Open the file in append mode, create it if it doesn't exist
+ file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ // Write the content to the file
+ _, err = file.WriteString(content)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func generateMarkdown() error {
+ filePrepender := func(filename string) string {
+ return ""
+ }
+ linkHandler := func(name string) string {
+ base := strings.TrimSuffix(name, path.Ext(name))
+ if base == cmd.Name {
+ return "/README.md"
+ }
+ return "/man/" + strings.ToLower(base) + ".md"
+ }
+ return doc.GenMarkdownTreeCustom(cmd.RootCmd, "./man", filePrepender, linkHandler)
+}
+
+// replaceBetweenStrings replaces everything between startString and endString with replacementText in the content of the file
+func replaceBetweenStrings(filePath, startString, endString, replacementText string) error {
+ // Read the file content
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return err
+ }
+
+ // Convert content to string
+ text := string(content)
+
+ // Find the start and end boundaries
+ startIndex := strings.Index(text, startString)
+ endIndex := strings.Index(text, endString)
+ if startIndex == -1 || endIndex == -1 || startIndex >= endIndex {
+ return fmt.Errorf("could not find valid boundaries between '%s' and '%s'", startString, endString)
+ }
+
+ // Build the new content
+ // Preserve everything before and after the boundaries, and insert the replacement text in-between
+ newContent := text[:startIndex] + replacementText + text[endIndex+len(endString):]
+
+ // Write the modified content back to the file
+ err = os.WriteFile(filePath, []byte(newContent), 0644)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+func updateBanner(filePath string) error {
+ textBanner := ansi.Strip(style.BANNER)
+ textSplit := strings.Split(textBanner, "\n")
+ return replaceBetweenStrings(filePath, textSplit[1], textSplit[len(textSplit)-2], "")
+}
+func updateBanners(dirPath string, starlight bool) error {
+ // Open the directory
+ dir, err := os.Open(dirPath)
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+
+ // Read directory entries
+ files, err := dir.Readdir(-1) // `-1` reads all entries in the directory
+ if err != nil {
+ return err
+ }
+
+ // Iterate over all files and directories
+ for _, file := range files {
+ if file.IsDir() {
+ fmt.Printf("Skipping directory: %s\n", file.Name())
+ continue
+ }
+ if strings.HasSuffix(file.Name(), ".md") && strings.HasPrefix(file.Name(), cmd.Name) {
+ if starlight {
+ err = updateStarlightHeadings(dirPath + file.Name())
+ if err != nil {
+ return err
+ }
+ } else {
+ err = updateBanner(dirPath + file.Name())
+ if err != nil {
+ return err
+ }
+ }
+
+ }
+ }
+
+ return nil
+}
+
+func updateStarlightHeadings(filePath string) error {
+ textBanner := ansi.Strip(style.BANNER)
+ textSplit := strings.Split(textBanner, "\n")
+ return replaceBetweenStrings(filePath, "## nodekit", textSplit[len(textSplit)-2], "## Synopsis")
+}
+
+const fmTemplate = `---
+title: "%s"
+slug: "%s"
+---
+`
+
+func generateStarlightMarkdown() error {
+ filePrepender := func(filename string) string {
+ name := filepath.Base(filename)
+ base := strings.TrimSuffix(name, path.Ext(name))
+
+ return fmt.Sprintf(fmTemplate, strings.Replace(base, "_", " ", -1), fmt.Sprintf("reference/%s", strings.Replace(base, "_", "/", -1)))
+ }
+ linkHandler := func(name string) string {
+ base := strings.TrimSuffix(name, path.Ext(name))
+ return "/reference/" + strings.Replace(base, "_", "/", -1)
+ }
+ return doc.GenMarkdownTreeCustom(cmd.RootCmd, "./docs/src/content/docs/reference", filePrepender, linkHandler)
+}
+
+func main() {
+ err := generateMarkdown()
+ if err != nil {
+ panic(err)
+ }
+
+ rootCmdDocPath := fmt.Sprintf("./man/%s.md", cmd.Name)
+
+ err = updateBanners("./man/", false)
+ if err != nil {
+ panic(err)
+ }
+ // Add Footer
+ footerDocPath := "./assets/footer.md"
+ footerBytes, err := os.ReadFile(footerDocPath)
+ if err != nil {
+ panic(err)
+ }
+ err = appendString(rootCmdDocPath, "\n"+string(footerBytes))
+ if err != nil {
+ panic(err)
+ }
+ err = copyFile(fmt.Sprintf("./man/%s.md", cmd.Name), "./README.md", true)
+ if err != nil {
+ panic(err)
+ }
+
+ err = generateStarlightMarkdown()
+ if err != nil {
+ panic(err)
+ }
+
+ err = updateBanners("./docs/src/content/docs/reference/", true)
+}
diff --git a/ui/README.md b/ui/README.md
index a50a9d1e..9868b500 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -1,13 +1,13 @@
# Overview
-The ui package contains bubbletea interfaces.
+The ui package contains bubbletea interfaces.
## Common practices
A `style.go` file holds lipgloss predefined styles for the package.
All components are instances of a `tea.Model` which is composed of models
-from the `internal` package.
+from the `internal` package.
Components can either be single file or independent packages.
Example for `status.go` single file component:
@@ -15,7 +15,7 @@ Example for `status.go` single file component:
```go
package ui
-import "github.com/algorandfoundation/algorun-tui/internal"
+import "github.com/algorandfoundation/nodekit/internal"
type StatusViewModel struct {
Data internal.StateModel
@@ -26,15 +26,16 @@ func (m StatusViewModel) Int(){}
//other tea.Model interfaces ...
```
-Once the component is sufficiently complex or needs to be reused, it can be moved
+Once the component is sufficiently complex or needs to be reused, it can be moved
to its own package
Example refactor for `status.go` to a package:
#### ui/status/model.go
+
```go
package status
-import "github.com/algorandfoundation/algorun-tui/internal"
+import "github.com/algorandfoundation/nodekit/internal"
type ViewModel struct {
Data internal.StateModel
@@ -43,6 +44,7 @@ type ViewModel struct {
```
#### ui/status/controller.go
+
```go
package status
@@ -84,4 +86,4 @@ package status
import "github.com/charmbracelet/lipgloss"
var someStyle = lipgloss.NewStyle()
-```
\ No newline at end of file
+```
diff --git a/ui/app/accounts.go b/ui/app/accounts.go
index 1287fac4..7b219a4c 100644
--- a/ui/app/accounts.go
+++ b/ui/app/accounts.go
@@ -1,14 +1,15 @@
package app
import (
- "github.com/algorandfoundation/algorun-tui/internal"
+ "github.com/algorandfoundation/nodekit/internal/algod"
tea "github.com/charmbracelet/bubbletea"
)
-type AccountSelected internal.Account
+// AccountSelected is a type alias for algod.Account, representing a selected account during application runtime.
+type AccountSelected algod.Account
// EmitAccountSelected waits for and retrieves a new set of table rows from a given channel.
-func EmitAccountSelected(account internal.Account) tea.Cmd {
+func EmitAccountSelected(account algod.Account) tea.Cmd {
return func() tea.Msg {
return AccountSelected(account)
}
diff --git a/ui/app/app_test.go b/ui/app/app_test.go
index 311ffdb4..f546b43a 100644
--- a/ui/app/app_test.go
+++ b/ui/app/app_test.go
@@ -2,16 +2,16 @@ package app
import (
"context"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/internal/test"
- uitest "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/internal/test"
+ uitest "github.com/algorandfoundation/nodekit/ui/internal/test"
"testing"
"time"
)
func Test_GenerateCmd(t *testing.T) {
client := test.GetClient(false)
- fn := GenerateCmd("ABC", internal.TimeRange, int(time.Second*60), uitest.GetState(client))
+ fn := GenerateCmd("ABC", participation.TimeRange, int(time.Second*60), uitest.GetState(client))
res := fn()
evt, ok := res.(ModalEvent)
if !ok {
@@ -22,7 +22,7 @@ func Test_GenerateCmd(t *testing.T) {
}
client = test.GetClient(true)
- fn = GenerateCmd("ABC", internal.TimeRange, int(time.Second*60), uitest.GetState(client))
+ fn = GenerateCmd("ABC", participation.TimeRange, int(time.Second*60), uitest.GetState(client))
res = fn()
evt, ok = res.(ModalEvent)
if !ok {
@@ -46,7 +46,7 @@ func Test_EmitDeleteKey(t *testing.T) {
t.Error("Expected ABC")
}
if evt.Err != nil {
- t.Error("Expected no errors")
+ t.Error("Expected no msgs")
}
client = test.GetClient(true)
@@ -60,7 +60,7 @@ func Test_EmitDeleteKey(t *testing.T) {
t.Error("Expected no response")
}
if evt.Err == nil {
- t.Error("Expected errors")
+ t.Error("Expected msgs")
}
}
diff --git a/ui/app/bootstrap.go b/ui/app/bootstrap.go
new file mode 100644
index 00000000..5b3cea8c
--- /dev/null
+++ b/ui/app/bootstrap.go
@@ -0,0 +1,19 @@
+package app
+
+import (
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+type BootstrapMsg struct {
+ Install bool
+ Catchup bool
+}
+
+type BoostrapSelected BootstrapMsg
+
+// EmitBootstrapSelection waits for and retrieves a new set of table rows from a given channel.
+func EmitBootstrapSelection(selection BoostrapSelected) tea.Cmd {
+ return func() tea.Msg {
+ return selection
+ }
+}
diff --git a/ui/app/emitter.go b/ui/app/emitter.go
new file mode 100644
index 00000000..fa4514cd
--- /dev/null
+++ b/ui/app/emitter.go
@@ -0,0 +1,16 @@
+package app
+
+import tea "github.com/charmbracelet/bubbletea"
+
+type Outside chan tea.Msg
+
+func NewOutside() Outside {
+ return make(chan tea.Msg)
+}
+
+func (o Outside) Emit(msg tea.Msg) tea.Cmd {
+ return func() tea.Msg {
+ o <- msg
+ return nil
+ }
+}
diff --git a/ui/app/keys.go b/ui/app/keys.go
index f5eb4ead..7dc134bc 100644
--- a/ui/app/keys.go
+++ b/ui/app/keys.go
@@ -2,23 +2,25 @@ package app
import (
"context"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/charmbracelet/lipgloss"
"time"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
+ "github.com/algorandfoundation/nodekit/api"
tea "github.com/charmbracelet/bubbletea"
)
+// DeleteFinished represents the result of a deletion operation, containing an optional error and the associated ID.
type DeleteFinished struct {
Err *error
Id string
}
-type DeleteKey *api.ParticipationKey
-
+// EmitDeleteKey creates a command to delete a participation key by ID and returns the result as a DeleteFinished message.
func EmitDeleteKey(ctx context.Context, client api.ClientWithResponsesInterface, id string) tea.Cmd {
return func() tea.Msg {
- err := internal.DeletePartKey(ctx, client, id)
+ err := participation.Delete(ctx, client, id)
if err != nil {
return DeleteFinished{
Err: &err,
@@ -32,11 +34,13 @@ func EmitDeleteKey(ctx context.Context, client api.ClientWithResponsesInterface,
}
}
-func GenerateCmd(account string, rangeType internal.RangeType, duration int, state *internal.StateModel) tea.Cmd {
+// GenerateCmd creates a command to generate participation keys for a specified account using given range type and duration.
+// It utilizes the current state to configure the parameters required for key generation and returns a ModalEvent as a message.
+func GenerateCmd(account string, rangeType participation.RangeType, duration int, state *algod.StateModel) tea.Cmd {
return func() tea.Msg {
var params api.GenerateParticipationKeysParams
- if rangeType == internal.TimeRange {
+ if rangeType == participation.TimeRange {
params = api.GenerateParticipationKeysParams{
Dilution: nil,
First: int(state.Status.LastRound),
@@ -50,13 +54,13 @@ func GenerateCmd(account string, rangeType internal.RangeType, duration int, sta
}
}
- key, err := internal.GenerateKeyPair(state.Context, state.Client, account, ¶ms)
+ key, err := participation.GenerateKeys(state.Context, state.Client, account, ¶ms)
if err != nil {
return ModalEvent{
Key: nil,
Address: "",
Active: false,
- Err: &err,
+ Err: err,
Type: ExceptionModal,
}
}
@@ -64,9 +68,17 @@ func GenerateCmd(account string, rangeType internal.RangeType, duration int, sta
return ModalEvent{
Key: key,
Address: key.Address,
- Active: false,
- Err: nil,
- Type: InfoModal,
+ Prefix: lipgloss.JoinVertical(
+ lipgloss.Left,
+ "Participation keys generated.",
+ "",
+ "Next step: register the participation keys with the network by signing a keyreg online transaction.",
+ "Press the R key to start this process.",
+ "",
+ ),
+ Active: false,
+ Err: nil,
+ Type: InfoModal,
}
}
diff --git a/ui/app/modal.go b/ui/app/modal.go
index 18b72716..e84b0edf 100644
--- a/ui/app/modal.go
+++ b/ui/app/modal.go
@@ -1,36 +1,67 @@
package app
import (
- "github.com/algorandfoundation/algorun-tui/api"
+ "github.com/algorandfoundation/nodekit/api"
tea "github.com/charmbracelet/bubbletea"
)
+// ModalType represents the type of modal to be displayed in the application.
type ModalType string
const (
- CloseModal ModalType = ""
- CancelModal ModalType = "cancel"
- InfoModal ModalType = "info"
- ConfirmModal ModalType = "confirm"
+
+ // CloseModal represents an event or type used to close the currently active modal in the application.
+ CloseModal ModalType = ""
+
+ // CancelModal is a constant representing the type for modals used to indicate cancellation events in the application.
+ CancelModal ModalType = "cancel"
+
+ // InfoModal indicates a modal type used for displaying informational messages or content in the application.
+ InfoModal ModalType = "info"
+
+ // ConfirmModal represents a modal type used for user confirmation actions in the application.
+ ConfirmModal ModalType = "confirm"
+
+ // TransactionModal represents a modal type used for handling transaction-related actions or displays in the application.
TransactionModal ModalType = "transaction"
- GenerateModal ModalType = "generate"
- ExceptionModal ModalType = "exception"
+
+ // GenerateModal represents a modal type used for generating or creating items or content within the application.
+ GenerateModal ModalType = "generate"
+
+ // ExceptionModal represents a modal type used for displaying errors or exceptions within the application.
+ ExceptionModal ModalType = "exception"
)
+// EmitShowModal creates a command to emit a modal message of the specified ModalType.
func EmitShowModal(modal ModalType) tea.Cmd {
return func() tea.Msg {
return modal
}
}
+// ModalEvent represents an event triggered in the modal system.
type ModalEvent struct {
- Key *api.ParticipationKey
- Active bool
+
+ // Key represents a participation key associated with the modal event.
+ Key *api.ParticipationKey
+
+ // Active indicates whether key is Online or not.
+ Active bool
+
+ // Address represents the address associated with the modal event. It is used to identify the relevant account or key.
Address string
- Err *error
- Type ModalType
+
+ // Prefix adds prefix message to info modal
+ Prefix string
+
+ // Err is an error that represents an exceptional condition or failure state for the modal event.
+ Err error
+
+ // Type represents the specific category or variant of the modal event.
+ Type ModalType
}
+// EmitModalEvent creates a command that emits a ModalEvent as a message in the Tea framework.
func EmitModalEvent(event ModalEvent) tea.Cmd {
return func() tea.Msg {
return event
diff --git a/ui/app/url.go b/ui/app/url.go
new file mode 100644
index 00000000..d2bfab5c
--- /dev/null
+++ b/ui/app/url.go
@@ -0,0 +1,56 @@
+package app
+
+import (
+ "encoding/base64"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "strings"
+
+ "github.com/algorandfoundation/nodekit/api"
+ tea "github.com/charmbracelet/bubbletea"
+)
+
+func EmitCreateShortLink(offline bool, part *api.ParticipationKey, state *algod.StateModel) tea.Cmd {
+ if part == nil || state == nil {
+ return nil
+ }
+
+ var loraNetwork = strings.Replace(strings.Replace(state.Status.Network, "-v1.0", "", 1), "-v1", "", 1)
+ if loraNetwork == "dockernet" || loraNetwork == "tuinet" {
+ loraNetwork = "localnet"
+ }
+
+ if offline {
+ res, err := participation.GetOfflineShortLink(state.HttpPkg, participation.OfflineShortLinkBody{
+ Account: part.Address,
+ Network: loraNetwork,
+ })
+ if err != nil {
+ return func() tea.Msg {
+ return err
+ }
+ }
+ return func() tea.Msg {
+ return res
+ }
+ }
+
+ res, err := participation.GetOnlineShortLink(state.HttpPkg, participation.OnlineShortLinkBody{
+ Account: part.Address,
+ VoteKeyB64: base64.RawURLEncoding.EncodeToString(part.Key.VoteParticipationKey),
+ SelectionKeyB64: base64.RawURLEncoding.EncodeToString(part.Key.SelectionParticipationKey),
+ StateProofKeyB64: base64.RawURLEncoding.EncodeToString(*part.Key.StateProofKey),
+ VoteFirstValid: part.Key.VoteFirstValid,
+ VoteLastValid: part.Key.VoteLastValid,
+ KeyDilution: part.Key.VoteKeyDilution,
+ Network: loraNetwork,
+ })
+ if err != nil {
+ return func() tea.Msg {
+ return err
+ }
+ }
+ return func() tea.Msg {
+ return res
+ }
+}
diff --git a/ui/app/viewport.go b/ui/app/viewport.go
index a7ed03f2..6c9ed978 100644
--- a/ui/app/viewport.go
+++ b/ui/app/viewport.go
@@ -8,10 +8,15 @@ import (
type Page string
const (
+
+ // AccountsPage represents the page within the application used for managing and displaying account information.
AccountsPage Page = "accounts"
- KeysPage Page = "keys"
+
+ // KeysPage represents the page within the application used for managing and displaying key-related information.
+ KeysPage Page = "keys"
)
+// EmitShowPage returns a command that emits a tea.Msg containing the given Page to be displayed in the application's viewport.
func EmitShowPage(page Page) tea.Cmd {
return func() tea.Msg {
return page
diff --git a/ui/bootstrap/model.go b/ui/bootstrap/model.go
new file mode 100644
index 00000000..23ce2a25
--- /dev/null
+++ b/ui/bootstrap/model.go
@@ -0,0 +1,103 @@
+package bootstrap
+
+import (
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+)
+
+type Question string
+
+const (
+ InstallQuestion Question = "install"
+ CatchupQuestion Question = "catchup"
+ WaitingQuestion Question = "waiting"
+)
+
+const InstallQuestionMsg = `# Installing A Node
+
+It looks like you're running this for the first time. Would you like to install a node? (Y/n)
+`
+
+const CatchupQuestionMsg = `# Catching Up
+
+Regular sync with the network usually takes multiple days to weeks. You can optionally perform fast-catchup to sync within minutes instead.
+
+Would you like to preform a fast-catchup after installation? (Y/n)
+`
+
+type Model struct {
+ Outside app.Outside
+ BootstrapMsg app.BootstrapMsg
+ Question Question
+}
+
+func NewModel() Model {
+ return Model{
+ Outside: make(app.Outside),
+ Question: InstallQuestion,
+ BootstrapMsg: app.BootstrapMsg{
+ Install: false,
+ Catchup: false,
+ },
+ }
+}
+func (m Model) Init() tea.Cmd {
+ return textinput.Blink
+}
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ m.Outside.Emit(msg)
+ if m.Question == WaitingQuestion {
+ return m, tea.Sequence(m.Outside.Emit(m.BootstrapMsg), tea.Quit)
+ }
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "y":
+ {
+ switch m.Question {
+ case InstallQuestion:
+ m.Question = CatchupQuestion
+ m.BootstrapMsg.Install = true
+ case CatchupQuestion:
+ m.BootstrapMsg.Catchup = true
+ m.Question = WaitingQuestion
+ return m, app.EmitBootstrapSelection(app.BoostrapSelected(m.BootstrapMsg))
+ }
+
+ }
+ case "n":
+ {
+ switch m.Question {
+ case InstallQuestion:
+ m.Question = CatchupQuestion
+ m.BootstrapMsg.Install = true
+ case CatchupQuestion:
+ m.Question = WaitingQuestion
+ m.BootstrapMsg.Catchup = true
+ case WaitingQuestion:
+ return m, tea.Sequence(m.Outside.Emit(m.BootstrapMsg), tea.Quit)
+ }
+
+ }
+
+ case "ctrl+c", "esc", "q":
+ return m, tea.Quit
+ }
+ }
+ return m, cmd
+}
+
+func (m Model) View() string {
+ var str string
+ switch m.Question {
+ case InstallQuestion:
+ str = InstallQuestionMsg
+ case CatchupQuestion:
+ str = CatchupQuestionMsg
+ }
+ msg, _ := glamour.Render(str, "dark")
+ return msg
+}
diff --git a/ui/explanations/explanations.go b/ui/explanations/explanations.go
deleted file mode 100644
index 7b4c6985..00000000
--- a/ui/explanations/explanations.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package explanations
-
-import (
- "github.com/algorandfoundation/algorun-tui/ui/style"
-)
-
-var NodeNotFound = "\n\nExplanation: algorun could not find your node automatically. Provide --algod-endpoint and --algod-token, or set the goal-compatible ALGORAND_DATA environment variable to the algod data directory, e.g. /var/lib/algorand\n"
-
-var Unreachable = "\n\nExplanation: Could not reach algod. Check that algod is running and the provided connection arguments.\n"
-
-var TokenInvalid = "\n\nExplanation: algod token is invalid. Algorun requires the " + style.BoldUnderline("admin token") + " for algod. You can find this in the algod.admin.token file in the algod data directory.\n"
-
-var TokenNotAdmin = "\n\nExplanation: algorun requires the " + style.BoldUnderline("admin token") + " for algod. You can find this in the algod.admin.token file in the algod data directory.\n"
diff --git a/ui/internal/test/state.go b/ui/internal/test/state.go
index 3f159e95..d8471201 100644
--- a/ui/internal/test/state.go
+++ b/ui/internal/test/state.go
@@ -2,23 +2,26 @@ package test
import (
"context"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- mock2 "github.com/algorandfoundation/algorun-tui/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ mock2 "github.com/algorandfoundation/nodekit/internal/test/mock"
"time"
)
-func GetState(client api.ClientWithResponsesInterface) *internal.StateModel {
- sm := &internal.StateModel{
- Status: internal.StatusModel{
- State: internal.StableState,
+func GetState(client api.ClientWithResponsesInterface) *algod.StateModel {
+ sm := &algod.StateModel{
+ Status: algod.Status{
+ State: algod.StableState,
Version: "v-test",
Network: "v-test-network",
Voting: false,
NeedsUpdate: false,
LastRound: 0,
+
+ Client: client,
+ HttpPkg: new(api.HttpPkg),
},
- Metrics: internal.MetricsModel{
+ Metrics: algod.Metrics{
Enabled: true,
Window: 100,
RoundTime: time.Second * 2,
@@ -30,17 +33,18 @@ func GetState(client api.ClientWithResponsesInterface) *internal.StateModel {
LastTX: 0,
},
Accounts: nil,
- ParticipationKeys: &mock2.Keys,
+ ParticipationKeys: mock2.Keys,
Admin: false,
Watching: false,
Client: client,
+ HttpPkg: new(api.HttpPkg),
Context: context.Background(),
}
- values := make(map[string]internal.Account)
- for _, key := range *sm.ParticipationKeys {
+ values := make(map[string]algod.Account)
+ for _, key := range sm.ParticipationKeys {
val, ok := values[key.Address]
if !ok {
- values[key.Address] = internal.Account{
+ values[key.Address] = algod.Account{
Address: key.Address,
Status: "Offline",
Balance: 0,
diff --git a/ui/modal/controller.go b/ui/modal/controller.go
index 06a698ed..b3672596 100644
--- a/ui/modal/controller.go
+++ b/ui/modal/controller.go
@@ -1,14 +1,18 @@
package modal
import (
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/modals/generate"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/modals/generate"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
+ "time"
)
+// Init initializes the current ViewModel by batching initialization commands for all associated modal ViewModels.
func (m ViewModel) Init() tea.Cmd {
return tea.Batch(
m.infoModal.Init(),
@@ -18,6 +22,8 @@ func (m ViewModel) Init() tea.Cmd {
m.generateModal.Init(),
)
}
+
+// HandleMessage processes the given message, updates the ViewModel state, and returns any commands to execute.
func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
var (
cmd tea.Cmd
@@ -28,13 +34,43 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
m.Open = true
m.exceptionModal.Message = msg.Error()
m.SetType(app.ExceptionModal)
- case internal.StateModel:
- m.State = &msg
- m.transactionModal.State = &msg
- m.infoModal.State = &msg
+ case participation.ShortLinkResponse:
+ m.Open = true
+ m.SetShortLink(msg)
+ m.SetType(app.TransactionModal)
+ case *algod.StateModel:
+ // Clear the catchup modal
+ if msg.Status.State != algod.FastCatchupState && m.Type == app.ExceptionModal && m.title == "Fast Catchup" {
+ m.Open = false
+ m.SetType(app.InfoModal)
+ }
+
+ m.State = msg
+ m.transactionModal.State = msg
+ m.infoModal.State = msg
+
+ // On Fast-Catchup, handle the state as an exception modal
+ if m.State.Status.State == algod.FastCatchupState {
+ m.Open = true
+ m.SetType(app.ExceptionModal)
+ m.exceptionModal.Message = style.LightBlue(lipgloss.JoinVertical(lipgloss.Top,
+ "Please wait while your node syncs with the network.",
+ "This process can take up to an hour.",
+ "",
+ fmt.Sprintf("Accounts Processed: %d / %d", m.State.Status.CatchpointAccountsProcessed, m.State.Status.CatchpointAccountsTotal),
+ fmt.Sprintf("Accounts Verified: %d / %d", m.State.Status.CatchpointAccountsVerified, m.State.Status.CatchpointAccountsTotal),
+ fmt.Sprintf("Key Values Processed: %d / %d", m.State.Status.CatchpointKeyValueProcessed, m.State.Status.CatchpointKeyValueTotal),
+ fmt.Sprintf("Key Values Verified: %d / %d", m.State.Status.CatchpointKeyValueVerified, m.State.Status.CatchpointKeyValueTotal),
+ fmt.Sprintf("Downloaded blocks: %d / %d", m.State.Status.CatchpointBlocksAcquired, m.State.Status.CatchpointBlocksTotal),
+ "",
+ fmt.Sprintf("Sync Time: %ds", m.State.Status.SyncTime/int(time.Second)),
+ ))
+ m.borderColor = "7"
+ m.controls = ""
+ m.title = "Fast Catchup"
- // When the state changes, and we are displaying a valid QR Code/Transaction Modal
- if m.Type == app.TransactionModal && m.transactionModal.Participation != nil {
+ } else if m.Type == app.TransactionModal && m.transactionModal.Participation != nil {
+ // Get the existing account from the state
acct, ok := msg.Accounts[m.Address]
// If the previous state is not active
if ok {
@@ -43,6 +79,8 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
acct.Participation.VoteFirstValid == m.transactionModal.Participation.Key.VoteFirstValid {
m.SetActive(true)
m.infoModal.Active = true
+ m.infoModal.Prefix = "Successfully registered online!\n"
+ m.HasPrefix = true
m.SetType(app.InfoModal)
}
} else {
@@ -58,7 +96,15 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
}
case app.ModalEvent:
+ if msg.Type == app.ExceptionModal {
+ m.Open = true
+ m.exceptionModal.Message = msg.Err.Error()
+ m.generateModal.SetStep(generate.AddressStep)
+ m.SetType(app.ExceptionModal)
+ }
+
if msg.Type == app.InfoModal {
+ m.infoModal.Prefix = msg.Prefix
m.generateModal.SetStep(generate.AddressStep)
}
// On closing events
@@ -151,6 +197,8 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
return &m, tea.Batch(cmds...)
}
+
+// Update processes the given message, updates the ViewModel state, and returns the updated model and accompanying commands.
func (m ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.HandleMessage(msg)
}
diff --git a/ui/modal/modal_test.go b/ui/modal/modal_test.go
index 44b17774..7c43ba2c 100644
--- a/ui/modal/modal_test.go
+++ b/ui/modal/modal_test.go
@@ -3,9 +3,10 @@ package modal
import (
"bytes"
"errors"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
@@ -60,6 +61,9 @@ func Test_Snapshot(t *testing.T) {
t.Run("TransactionModal", func(t *testing.T) {
model := New(lipgloss.NewStyle().Width(80).Height(80).Render(""), true, test.GetState(nil))
model.State.Status.Network = "testnet-v1.0"
+ model.SetShortLink(participation.ShortLinkResponse{
+ Id: "1234",
+ })
model.SetKey(&mock.Keys[0])
model.SetActive(true)
model.SetType(app.TransactionModal)
diff --git a/ui/modal/model.go b/ui/modal/model.go
index b720886d..599eaeeb 100644
--- a/ui/modal/model.go
+++ b/ui/modal/model.go
@@ -1,14 +1,15 @@
package modal
import (
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/modals/confirm"
- "github.com/algorandfoundation/algorun-tui/ui/modals/exception"
- "github.com/algorandfoundation/algorun-tui/ui/modals/generate"
- "github.com/algorandfoundation/algorun-tui/ui/modals/info"
- "github.com/algorandfoundation/algorun-tui/ui/modals/transaction"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/modals/confirm"
+ "github.com/algorandfoundation/nodekit/ui/modals/exception"
+ "github.com/algorandfoundation/nodekit/ui/modals/generate"
+ "github.com/algorandfoundation/nodekit/ui/modals/info"
+ "github.com/algorandfoundation/nodekit/ui/modals/transaction"
)
type ViewModel struct {
@@ -22,10 +23,17 @@ type ViewModel struct {
Height int
// State for Context/Client
- State *internal.StateModel
+ State *algod.StateModel
// Address defines the string format address of the entity
Address string
+ // HasPrefix indicates whether a prefix is used or active.
+ HasPrefix bool
+
+ // Link represents a reference to a ShortLinkResponse,
+ // typically used for processing or displaying shortened link data.
+ Link *participation.ShortLinkResponse
+
// Views
infoModal *info.ViewModel
transactionModal *transaction.ViewModel
@@ -40,15 +48,20 @@ type ViewModel struct {
Type app.ModalType
}
+// SetAddress updates the ViewModel's Address property and synchronizes it with the associated generateModal.
func (m *ViewModel) SetAddress(address string) {
m.Address = address
m.generateModal.SetAddress(address)
}
+
+// SetKey updates the participation key across infoModal, confirmModal, and transactionModal in the ViewModel.
func (m *ViewModel) SetKey(key *api.ParticipationKey) {
m.infoModal.Participation = key
m.confirmModal.ActiveKey = key
m.transactionModal.Participation = key
}
+
+// SetActive sets the active state for both infoModal and transactionModal, and updates their respective states.
func (m *ViewModel) SetActive(active bool) {
m.infoModal.Active = active
m.infoModal.UpdateState()
@@ -56,6 +69,12 @@ func (m *ViewModel) SetActive(active bool) {
m.transactionModal.UpdateState()
}
+func (m *ViewModel) SetShortLink(res participation.ShortLinkResponse) {
+ m.Link = &res
+ m.transactionModal.Link = &res
+}
+
+// SetType updates the modal type of the ViewModel and configures its title, controls, and border color accordingly.
func (m *ViewModel) SetType(modal app.ModalType) {
m.Type = modal
switch modal {
@@ -82,7 +101,8 @@ func (m *ViewModel) SetType(modal app.ModalType) {
}
}
-func New(parent string, open bool, state *internal.StateModel) *ViewModel {
+// New initializes and returns a new ViewModel with the specified parent, open state, and application StateModel.
+func New(parent string, open bool, state *algod.StateModel) *ViewModel {
return &ViewModel{
Parent: parent,
Open: open,
@@ -90,8 +110,9 @@ func New(parent string, open bool, state *internal.StateModel) *ViewModel {
Width: 0,
Height: 0,
- Address: "",
- State: state,
+ Address: "",
+ HasPrefix: false,
+ State: state,
infoModal: info.New(state),
transactionModal: transaction.New(state),
diff --git a/ui/modal/testdata/Test_Snapshot/InfoModal.golden b/ui/modal/testdata/Test_Snapshot/InfoModal.golden
index d35f7740..3e28416a 100644
--- a/ui/modal/testdata/Test_Snapshot/InfoModal.golden
+++ b/ui/modal/testdata/Test_Snapshot/InfoModal.golden
@@ -36,8 +36,8 @@
โ Account: ABC โ
โ Participation ID: 123 โ
โ โ
- โ Selection Key: VEVTVEtFWQ โ
โ Vote Key: VEVTVEtFWQ โ
+ โ Selection Key: VEVTVEtFWQ โ
โ State Proof Key: VEVTVEtFWQ โ
โ โ
โ Vote First Valid: 0 โ
diff --git a/ui/modal/testdata/Test_Snapshot/TransactionModal.golden b/ui/modal/testdata/Test_Snapshot/TransactionModal.golden
index cfff39a4..f134ccd7 100644
--- a/ui/modal/testdata/Test_Snapshot/TransactionModal.golden
+++ b/ui/modal/testdata/Test_Snapshot/TransactionModal.golden
@@ -23,36 +23,36 @@
-
- โญโโRegister Offlineโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
- โ Sign this transaction to deregister your account keys: โ
- โ โ
- โ Scan the QR code with Pera or Defly โ
- โ (make sure you use the testnet-v1.0 network) โ
- โ โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
- โ โโ โโโโโ โโโโโโโโ โโ โโโโโ โโ โ
- โ โโ โ โ โโโโโ โ โ โ โ โ โโ โ
- โ โโ โโโโโ โโโโโโโ โโ โโโโโ โโ โ
- โ โโโโโโโโโโโโโโโโ โ โโโโโโโโโโ โ
- โ โโโโ โ โโโโ โโโ โโโโโโโโ โโโ โ
- โ โโโโ โโโโโโโโโโโโโโโโโโโโ โโโ โ
- โ โโโโโ โโ โ โโโโโโโโ โโโ โโโ โ
- โ โโโโโ โโ โโ โโ โโโโโโโโโโโโ โ
- โ โโโโโโโโโโโโโโ โโ โโโ โโ โโ โ
- โ โโ โโโโโ โโโ โโโโโ โโโ โโ โโ โ
- โ โโ โ โ โโ โโ โโโ โโ โโโโโโ โ
- โ โโ โโโโโ โโโ โโโ โโโโ โโโโโโ โ
- โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
- โ โ
- โ -or- โ
- โ โ
- โ Click here to sign via Lora. โ
- โ โ
- โ Note: this will take effect after 320 rounds (15 mins.) โ
- โ Please keep your node online during this cooldown period. โ
- โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ( esc )โโโโโฏ
-
+ โญโโRegister Offlineโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
+ โ Sign this transaction to register your account as offline โ
+ โ โ
+ โ Scan the QR code with Pera or Defly โ
+ โ (make sure you use the testnet-v1.0 network) โ
+ โ โ
+ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+ โ โโ โโโโโ โโโโโโโโ โโ โโโโโ โโ โ
+ โ โโ โ โ โโโโโ โ โ โ โ โ โโ โ
+ โ โโ โโโโโ โโโโโโโ โโ โโโโโ โโ โ
+ โ โโโโโโโโโโโโโโโโ โ โโโโโโโโโโ โ
+ โ โโโโ โ โโโโ โโโ โโโโโโโโ โโโ โ
+ โ โโโโ โโโโโโโโโโโโโโโโโโโโ โโโ โ
+ โ โโโโโ โโ โ โโโโโโโโ โโโ โโโ โ
+ โ โโโโโ โโ โโ โโ โโโโโโโโโโโโ โ
+ โ โโโโโโโโโโโโโโ โโ โโโ โโ โโ โ
+ โ โโ โโโโโ โโโ โโโโโ โโโ โโ โโ โ
+ โ โโ โ โ โโ โโ โโโ โโ โโโโโโ โ
+ โ โโ โโโโโ โโโ โโโ โโโโ โโโโโโ โ
+ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+ โ โ
+ โ -or- โ
+ โ โ
+ โ Open this URL in your browser: โ
+ โ โ
+ โ https://b.nodekit.run/1234 โ
+ โ โ
+ โ Note: this will take effect after 320 rounds (~15 min.) โ
+ โ Please keep your node running during this cooldown period. โ
+ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ( esc )โโโโโฏ
diff --git a/ui/modal/view.go b/ui/modal/view.go
index b6fa418e..ee2b0303 100644
--- a/ui/modal/view.go
+++ b/ui/modal/view.go
@@ -1,11 +1,12 @@
package modal
import (
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/style"
"github.com/charmbracelet/lipgloss"
)
+// View renders the current modal's UI based on its type and state, or returns the parent content if the modal is closed.
func (m ViewModel) View() string {
if !m.Open {
return m.Parent
diff --git a/ui/modals/confirm/confirm.go b/ui/modals/confirm/confirm.go
index 62b016f5..8080724a 100644
--- a/ui/modals/confirm/confirm.go
+++ b/ui/modals/confirm/confirm.go
@@ -1,25 +1,29 @@
package confirm
import (
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type ViewModel struct {
- Width int
+
+ // Width defines the horizontal dimension of the ViewModel, typically measured in units such as characters or pixels.
+ Width int
+
+ // Height defines the vertical dimension of the ViewModel, commonly measured in units such as characters or pixels.
Height int
Title string
Controls string
BorderColor string
ActiveKey *api.ParticipationKey
- Data *internal.StateModel
+ Data *algod.StateModel
}
-func New(state *internal.StateModel) *ViewModel {
+func New(state *algod.StateModel) *ViewModel {
return &ViewModel{
Width: 0,
Height: 0,
diff --git a/ui/modals/confirm/confirm_test.go b/ui/modals/confirm/confirm_test.go
index 0436d18e..6f6799de 100644
--- a/ui/modals/confirm/confirm_test.go
+++ b/ui/modals/confirm/confirm_test.go
@@ -2,8 +2,8 @@ package confirm
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
diff --git a/ui/modals/exception/error.go b/ui/modals/exception/error.go
index 63f92465..2fd45797 100644
--- a/ui/modals/exception/error.go
+++ b/ui/modals/exception/error.go
@@ -1,8 +1,8 @@
package exception
import (
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
diff --git a/ui/modals/generate/controller.go b/ui/modals/generate/controller.go
index 2df09791..db7728bd 100644
--- a/ui/modals/generate/controller.go
+++ b/ui/modals/generate/controller.go
@@ -1,11 +1,12 @@
package generate
import (
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
"strconv"
"time"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/app"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
@@ -76,7 +77,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
switch m.Step {
case AddressStep:
addr := m.Input.Value()
- if !internal.ValidateAddress(addr) {
+ if !algod.ValidateAddress(addr) {
m.InputError = "Error: invalid address"
return &m, nil
}
@@ -91,18 +92,18 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
}
m.InputTwoError = ""
m.SetStep(WaitingStep)
- var rangeType internal.RangeType
+ var rangeType participation.RangeType
var dur int
switch m.Range {
case Day:
dur = int(time.Hour*24) * val
- rangeType = internal.TimeRange
+ rangeType = participation.TimeRange
case Month:
dur = int(time.Hour*24*30) * val
- rangeType = internal.TimeRange
+ rangeType = participation.TimeRange
case Round:
dur = val
- rangeType = internal.RoundRange
+ rangeType = participation.RoundRange
}
return &m, tea.Sequence(app.EmitShowModal(app.GenerateModal), app.GenerateCmd(m.Input.Value(), rangeType, dur, m.State))
diff --git a/ui/modals/generate/generate_test.go b/ui/modals/generate/generate_test.go
index b1d71cea..f8c62b1f 100644
--- a/ui/modals/generate/generate_test.go
+++ b/ui/modals/generate/generate_test.go
@@ -2,7 +2,7 @@ package generate
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
diff --git a/ui/modals/generate/model.go b/ui/modals/generate/model.go
index 3abf78f2..0afd0a5e 100644
--- a/ui/modals/generate/model.go
+++ b/ui/modals/generate/model.go
@@ -1,7 +1,7 @@
package generate
import (
- "github.com/algorandfoundation/algorun-tui/internal"
+ "github.com/algorandfoundation/nodekit/internal/algod"
"github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
@@ -40,7 +40,7 @@ type ViewModel struct {
Controls string
BorderColor string
- State *internal.StateModel
+ State *algod.StateModel
cursorMode cursor.Mode
}
@@ -53,7 +53,7 @@ var DefaultControls = "( esc to cancel )"
var DefaultTitle = "Generate Consensus Participation Keys"
var DefaultBorderColor = "2"
-func New(address string, state *internal.StateModel) *ViewModel {
+func New(address string, state *algod.StateModel) *ViewModel {
input := textinput.New()
input2 := textinput.New()
diff --git a/ui/modals/generate/view.go b/ui/modals/generate/view.go
index 748f7356..79a377a1 100644
--- a/ui/modals/generate/view.go
+++ b/ui/modals/generate/view.go
@@ -3,7 +3,7 @@ package generate
import (
"fmt"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/style"
"github.com/charmbracelet/lipgloss"
)
diff --git a/ui/modals/info/info.go b/ui/modals/info/info.go
index cbe80cec..fa9b2073 100644
--- a/ui/modals/info/info.go
+++ b/ui/modals/info/info.go
@@ -1,11 +1,11 @@
package info
import (
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/style"
- "github.com/algorandfoundation/algorun-tui/ui/utils"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/utils"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
@@ -18,11 +18,12 @@ type ViewModel struct {
Controls string
BorderColor string
Active bool
+ Prefix string
Participation *api.ParticipationKey
- State *internal.StateModel
+ State *algod.StateModel
}
-func New(state *internal.StateModel) *ViewModel {
+func New(state *algod.StateModel) *ViewModel {
return &ViewModel{
Width: 0,
Height: 0,
@@ -53,8 +54,14 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
if !m.Active {
return &m, app.EmitShowModal(app.ConfirmModal)
}
+ case "r":
+ if !m.Active {
+ return &m, app.EmitCreateShortLink(m.Active, m.Participation, m.State)
+ }
case "o":
- return &m, app.EmitShowModal(app.TransactionModal)
+ if m.Active {
+ return &m, app.EmitCreateShortLink(m.Active, m.Participation, m.State)
+ }
}
case tea.WindowSizeMsg:
m.Width = msg.Width
@@ -76,7 +83,7 @@ func (m *ViewModel) UpdateState() {
if !m.Active {
m.BorderColor = "3"
- m.Controls = "( " + style.Red.Render("(d)elete") + " | take " + style.Green.Render("(o)nline") + " )"
+ m.Controls = "( " + style.Red.Render("(d)elete") + " | " + style.Green.Render("(r)egister") + " online )"
}
}
func (m ViewModel) View() string {
@@ -92,13 +99,18 @@ func (m ViewModel) View() string {
voteLastValid := style.Purple("Vote Last Valid: ") + utils.IntToStr(m.Participation.Key.VoteLastValid)
voteKeyDilution := style.Purple("Vote Key Dilution: ") + utils.IntToStr(m.Participation.Key.VoteKeyDilution)
+ prefix := ""
+ if m.Prefix != "" {
+ prefix = "\n" + m.Prefix
+ }
+
return ansi.Hardwrap(lipgloss.JoinVertical(lipgloss.Left,
- "",
+ prefix,
account,
id,
"",
- selection,
vote,
+ selection,
stateProof,
"",
voteFirstValid,
diff --git a/ui/modals/info/info_test.go b/ui/modals/info/info_test.go
index 4d869ffc..f3211e9f 100644
--- a/ui/modals/info/info_test.go
+++ b/ui/modals/info/info_test.go
@@ -2,8 +2,8 @@ package info
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
diff --git a/ui/modals/info/testdata/Test_Snapshot/Visible.golden b/ui/modals/info/testdata/Test_Snapshot/Visible.golden
index d258004b..e8bc0e8d 100644
--- a/ui/modals/info/testdata/Test_Snapshot/Visible.golden
+++ b/ui/modals/info/testdata/Test_Snapshot/Visible.golden
@@ -2,8 +2,8 @@
Account: ABC
Participation ID: 123
-Selection Key: VEVTVEtFWQ
Vote Key: VEVTVEtFWQ
+Selection Key: VEVTVEtFWQ
State Proof Key: VEVTVEtFWQ
Vote First Valid: 0
diff --git a/ui/modals/transaction/controller.go b/ui/modals/transaction/controller.go
index 0ff7b1b1..ac28515a 100644
--- a/ui/modals/transaction/controller.go
+++ b/ui/modals/transaction/controller.go
@@ -3,9 +3,9 @@ package transaction
import (
"encoding/base64"
"github.com/algorand/go-algorand-sdk/v2/types"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
"github.com/algorandfoundation/algourl/encoder"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/app"
tea "github.com/charmbracelet/bubbletea"
)
@@ -43,7 +43,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) {
m.UpdateState()
return &m, cmd
}
-func (m *ViewModel) Account() *internal.Account {
+func (m *ViewModel) Account() *algod.Account {
if m.Participation == nil || m.State == nil || m.State.Accounts == nil {
return nil
}
diff --git a/ui/modals/transaction/model.go b/ui/modals/transaction/model.go
index dadba88b..cfee432d 100644
--- a/ui/modals/transaction/model.go
+++ b/ui/modals/transaction/model.go
@@ -2,10 +2,11 @@ package transaction
import (
"fmt"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/style"
"github.com/algorandfoundation/algourl/encoder"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/ui/style"
)
type ViewModel struct {
@@ -19,9 +20,10 @@ type ViewModel struct {
// Active Participation Key
Participation *api.ParticipationKey
Active bool
+ Link *participation.ShortLinkResponse
// Pointer to the State
- State *internal.StateModel
+ State *algod.StateModel
IsOnline bool
// Components
@@ -38,7 +40,7 @@ func (m ViewModel) FormatedAddress() string {
}
// New creates and instance of the ViewModel with a default controls.Model
-func New(state *internal.StateModel) *ViewModel {
+func New(state *algod.StateModel) *ViewModel {
return &ViewModel{
State: state,
Title: "Offline Transaction",
diff --git a/ui/modals/transaction/testdata/Test_Snapshot/NotVisible.golden b/ui/modals/transaction/testdata/Test_Snapshot/NotVisible.golden
index 0b93e110..6e2c9e77 100644
--- a/ui/modals/transaction/testdata/Test_Snapshot/NotVisible.golden
+++ b/ui/modals/transaction/testdata/Test_Snapshot/NotVisible.golden
@@ -1,7 +1,9 @@
- Sign this transaction to register your account keys:
-
-Mobile QR is available but it does not fit on screen.
- Adjust terminal dimensions or font size to display.
-
- -or-
- Click here to sign via Lora.
\ No newline at end of file
+Sign this transaction to register your account as online
+
+ Mobile QR is available but it does not fit on screen.
+ Adjust terminal dimensions or font size to display.
+
+ -or-
+ Open this URL in your browser:
+
+ https://b.nodekit.run/1234
\ No newline at end of file
diff --git a/ui/modals/transaction/testdata/Test_Snapshot/Offline.golden b/ui/modals/transaction/testdata/Test_Snapshot/Offline.golden
index db090e46..456d04bc 100644
--- a/ui/modals/transaction/testdata/Test_Snapshot/Offline.golden
+++ b/ui/modals/transaction/testdata/Test_Snapshot/Offline.golden
@@ -1,26 +1,28 @@
- Sign this transaction to deregister your account keys:
-
- Scan the QR code with Pera or Defly
- (make sure you use the testnet-v1.0 network)
-
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โโ โโโโโ โโโโโโโโ โโ โโโโโ โโ
- โโ โ โ โโโโโ โ โ โ โ โ โโ
- โโ โโโโโ โโโโโโโ โโ โโโโโ โโ
- โโโโโโโโโโโโโโโโ โ โโโโโโโโโโ
- โโโโ โ โโโโ โโโ โโโโโโโโ โโโ
- โโโโ โโโโโโโโโโโโโโโโโโโโ โโโ
- โโโโโ โโ โ โโโโโโโโ โโโ โโโ
- โโโโโ โโ โโ โโ โโโโโโโโโโโโ
- โโโโโโโโโโโโโโ โโ โโโ โโ โโ
- โโ โโโโโ โโโ โโโโโ โโโ โโ โโ
- โโ โ โ โโ โโ โโโ โโ โโโโโโ
- โโ โโโโโ โโโ โโโ โโโโ โโโโโโ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- -or-
-
- Click here to sign via Lora.
-
- Note: this will take effect after 320 rounds (15 mins.)
-Please keep your node online during this cooldown period.
\ No newline at end of file
+ Sign this transaction to register your account as offline
+
+ Scan the QR code with Pera or Defly
+ (make sure you use the testnet-v1.0 network)
+
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โโ โโโโโ โโโโโโโโ โโ โโโโโ โโ
+ โโ โ โ โโโโโ โ โ โ โ โ โโ
+ โโ โโโโโ โโโโโโโ โโ โโโโโ โโ
+ โโโโโโโโโโโโโโโโ โ โโโโโโโโโโ
+ โโโโ โ โโโโ โโโ โโโโโโโโ โโโ
+ โโโโ โโโโโโโโโโโโโโโโโโโโ โโโ
+ โโโโโ โโ โ โโโโโโโโ โโโ โโโ
+ โโโโโ โโ โโ โโ โโโโโโโโโโโโ
+ โโโโโโโโโโโโโโ โโ โโโ โโ โโ
+ โโ โโโโโ โโโ โโโโโ โโโ โโ โโ
+ โโ โ โ โโ โโ โโโ โโ โโโโโโ
+ โโ โโโโโ โโโ โโโ โโโโ โโโโโโ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ -or-
+
+ Open this URL in your browser:
+
+ https://b.nodekit.run/1234
+
+ Note: this will take effect after 320 rounds (~15 min.)
+Please keep your node running during this cooldown period.
\ No newline at end of file
diff --git a/ui/modals/transaction/testdata/Test_Snapshot/Online.golden b/ui/modals/transaction/testdata/Test_Snapshot/Online.golden
index c022035b..c3d5ee9e 100644
--- a/ui/modals/transaction/testdata/Test_Snapshot/Online.golden
+++ b/ui/modals/transaction/testdata/Test_Snapshot/Online.golden
@@ -1,33 +1,35 @@
-Sign this transaction to register your account keys:
-
- Scan the QR code with Pera or Defly
- (make sure you use the testnet-v1.0 network)
-
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- โโ โโโโโ โโโโโโโโ โ โโโโโโโโ โ โ โ โโโโ โโโโโ โโ
- โโ โ โ โโ โ โโโโโ โโโโโโโโ โโโโ โโ โ โ โโ
- โโ โโโโโ โโโโโ โโโ โโ โโโ โ โโ โโโโโโโ โโโโโ โโ
- โโโโโโโโโโโโ โ โ โโโโโ โโโ โ โ โ โ โ โโโโโโโโโโโโ
- โโ โโโโโโโโโ โโโโ โโโ โโโโ โโโโโโ โโโโโโโ โโ โโโ
- โโโโ โ โโโโโโโ โโโโโโโ โโโโโโโ โโ โโ โโโโโโโโโโโ
- โโ โโโ โโโ โ โ โ โโ โโโโโ โโโโโโโโโโโโโโ โ โโโโโ
- โโโโโ โโโโโ โโโโโโโ โโโโโ โโ โโโโโโโโโโโ โโโโโ
- โโโโ โ โโโโโโโโโ โโโโโ โ โโโโโ โโ โโโโ โโโโโโ
- โโ โโ โ โ โโโโโโโโ โโโโโโ โ โโ โโโโโโ โโโโ โโ
- โโโโโโ โโโ โ โโโโโโ โ โโโ โ โโ โโโ โโโ โ โโ
- โโ โโโ โโโ โโ โโ โโ โ โโโ โ โโโ โ โโโ โโโ โโ
- โโโโโโ โโโโโโโโโโโ โโ โโโโ โโโ โโ โโโโโโโโ
- โโโโโโโโโโ โโโ โ โโ โโโโโ โโโโโโโโ โโโ โโโโโ
- โโโโโโโ โโโโโโโโโโโโ โโโโ โโ โโโ โโโ โโโโโโโ โโ
- โโโโ โโโโ โโโ โ โโโโโโโโ โ โโ โโ โโ โโ โโโโโโโโ
- โโโโโ โ โ โโโโโโโโโโ โ โโโโโ โโโโ โโ โโโโโโโโ
- โโโโโโ โโ โโโโโโโ โ โโ โโโ โโโ โโ โโ โโโโโโ โโ
- โโโโโโโโโโโ โโโ โ โโโ โโโ โโ โโ โโ โโโ โโ โโ
- โโ โโโโโ โโโโโโโโโโโ โ โโโ โโโ โโ โโโ โโโ โโ โโโ
- โโ โ โ โโโโโโโโโ โโโโโ โโโโโโโโโโโโ โโ โโโโโโ
- โโ โโโโโ โโโ โ โโโโโโ โโโโโโโโโโโโโโโโโโ โโ โโโ
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- -or-
-
- Click here to sign via Lora.
\ No newline at end of file
+Sign this transaction to register your account as online
+
+ Scan the QR code with Pera or Defly
+ (make sure you use the testnet-v1.0 network)
+
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โโ โโโโโ โโโโโโโโ โ โโโโโโโโ โ โ โ โโโโ โโโโโ โโ
+ โโ โ โ โโ โ โโโโโ โโโโโโโโ โโโโ โโ โ โ โโ
+ โโ โโโโโ โโโโโ โโโ โโ โโโ โ โโ โโโโโโโ โโโโโ โโ
+ โโโโโโโโโโโโ โ โ โโโโโ โโโ โ โ โ โ โ โโโโโโโโโโโโ
+ โโ โโโโโโโโโ โโโโ โโโ โโโโ โโโโโโ โโโโโโโ โโ โโโ
+ โโโโ โ โโโโโโโ โโโโโโโ โโโโโโโ โโ โโ โโโโโโโโโโโ
+ โโ โโโ โโโ โ โ โ โโ โโโโโ โโโโโโโโโโโโโโ โ โโโโโ
+ โโโโโ โโโโโ โโโโโโโ โโโโโ โโ โโโโโโโโโโโ โโโโโ
+ โโโโ โ โโโโโโโโโ โโโโโ โ โโโโโ โโ โโโโ โโโโโโ
+ โโ โโ โ โ โโโโโโโโ โโโโโโ โ โโ โโโโโโ โโโโ โโ
+ โโโโโโ โโโ โ โโโโโโ โ โโโ โ โโ โโโ โโโ โ โโ
+ โโ โโโ โโโ โโ โโ โโ โ โโโ โ โโโ โ โโโ โโโ โโ
+ โโโโโโ โโโโโโโโโโโ โโ โโโโ โโโ โโ โโโโโโโโ
+ โโโโโโโโโโ โโโ โ โโ โโโโโ โโโโโโโโ โโโ โโโโโ
+ โโโโโโโ โโโโโโโโโโโโ โโโโ โโ โโโ โโโ โโโโโโโ โโ
+ โโโโ โโโโ โโโ โ โโโโโโโโ โ โโ โโ โโ โโ โโโโโโโโ
+ โโโโโ โ โ โโโโโโโโโโ โ โโโโโ โโโโ โโ โโโโโโโโ
+ โโโโโโ โโ โโโโโโโ โ โโ โโโ โโโ โโ โโ โโโโโโ โโ
+ โโโโโโโโโโโ โโโ โ โโโ โโโ โโ โโ โโ โโโ โโ โโ
+ โโ โโโโโ โโโโโโโโโโโ โ โโโ โโโ โโ โโโ โโโ โโ โโโ
+ โโ โ โ โโโโโโโโโ โโโโโ โโโโโโโโโโโโ โโ โโโโโโ
+ โโ โโโโโ โโโ โ โโโโโโ โโโโโโโโโโโโโโโโโโ โโ โโโ
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ -or-
+
+ Open this URL in your browser:
+
+ https://b.nodekit.run/1234
\ No newline at end of file
diff --git a/ui/modals/transaction/testdata/Test_Snapshot/Unsupported.golden b/ui/modals/transaction/testdata/Test_Snapshot/Unsupported.golden
index dad0a92b..1e08af9b 100644
--- a/ui/modals/transaction/testdata/Test_Snapshot/Unsupported.golden
+++ b/ui/modals/transaction/testdata/Test_Snapshot/Unsupported.golden
@@ -1,5 +1,7 @@
-
-Sign this transaction to register your account keys:
-
- Click here to sign via Lora.
-
\ No newline at end of file
+
+Sign this transaction to register your account as online
+
+ Open this URL in your browser:
+
+ https://b.nodekit.run/1234
+
\ No newline at end of file
diff --git a/ui/modals/transaction/transaction_test.go b/ui/modals/transaction/transaction_test.go
index 57cc9621..fdd5bab2 100644
--- a/ui/modals/transaction/transaction_test.go
+++ b/ui/modals/transaction/transaction_test.go
@@ -2,8 +2,9 @@ package transaction
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
@@ -25,6 +26,9 @@ func Test_New(t *testing.T) {
func Test_Snapshot(t *testing.T) {
t.Run("NotVisible", func(t *testing.T) {
model := New(test.GetState(nil))
+ model.Link = &participation.ShortLinkResponse{
+ Id: "1234",
+ }
model.Participation = &mock.Keys[0]
model.UpdateState()
got := ansi.Strip(model.View())
@@ -32,6 +36,9 @@ func Test_Snapshot(t *testing.T) {
})
t.Run("Offline", func(t *testing.T) {
model := New(test.GetState(nil))
+ model.Link = &participation.ShortLinkResponse{
+ Id: "1234",
+ }
model.Participation = &mock.Keys[0]
model.State.Status.Network = "testnet-v1.0"
model, _ = model.HandleMessage(tea.WindowSizeMsg{
@@ -45,6 +52,9 @@ func Test_Snapshot(t *testing.T) {
})
t.Run("Online", func(t *testing.T) {
model := New(test.GetState(nil))
+ model.Link = &participation.ShortLinkResponse{
+ Id: "1234",
+ }
model.Participation = &mock.Keys[0]
model.State.Status.Network = "testnet-v1.0"
model, _ = model.HandleMessage(tea.WindowSizeMsg{
@@ -57,6 +67,9 @@ func Test_Snapshot(t *testing.T) {
})
t.Run("Unsupported", func(t *testing.T) {
model := New(test.GetState(nil))
+ model.Link = &participation.ShortLinkResponse{
+ Id: "1234",
+ }
model.Participation = &mock.Keys[0]
model, _ = model.HandleMessage(tea.WindowSizeMsg{
Height: 40,
@@ -82,6 +95,9 @@ func Test_Snapshot(t *testing.T) {
func Test_Messages(t *testing.T) {
// Create the Model
m := New(test.GetState(nil))
+ m.Link = &participation.ShortLinkResponse{
+ Id: "1234",
+ }
m.Participation = &mock.Keys[0]
m.State.Status.Network = "testnet-v1.0"
tm := teatest.NewTestModel(
diff --git a/ui/modals/transaction/view.go b/ui/modals/transaction/view.go
index b5201a31..d0f7c201 100644
--- a/ui/modals/transaction/view.go
+++ b/ui/modals/transaction/view.go
@@ -1,8 +1,9 @@
package transaction
import (
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "fmt"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/ui/style"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)
@@ -11,7 +12,7 @@ func (m ViewModel) View() string {
if m.Participation == nil {
return "No key selected"
}
- if m.ATxn == nil {
+ if m.ATxn == nil || m.Link == nil {
return "Loading..."
}
// TODO: Refactor ATxn to Interface
@@ -20,33 +21,32 @@ func (m ViewModel) View() string {
return "Something went wrong"
}
- var verb string
+ var adj string
isOffline := m.ATxn.AUrlTxnKeyreg.VotePK == nil
if isOffline {
- verb = "deregister"
+ adj = "offline"
} else {
- verb = "register"
+ adj = "online"
}
- intro := "Sign this transaction to " + verb + " your account keys:"
-
- link, _ := internal.ToLoraDeepLink(m.State.Status.Network, m.Active, m.Account().IncentiveEligible, *m.Participation)
- loraText := lipgloss.JoinHorizontal(
- lipgloss.Bottom,
- style.WithHyperlink("Click here", link),
- " to sign via Lora.",
+ intro := fmt.Sprintf("Sign this transaction to register your account as %s", adj)
+ link := participation.ToShortLink(*m.Link)
+ loraText := lipgloss.JoinVertical(
+ lipgloss.Center,
+ "Open this URL in your browser:\n",
+ style.WithHyperlink(link, link),
)
if isOffline {
loraText = lipgloss.JoinVertical(
lipgloss.Center,
loraText,
"",
- "Note: this will take effect after 320 rounds (15 mins.)",
- "Please keep your node online during this cooldown period.",
+ "Note: this will take effect after 320 rounds (~15 min.)",
+ "Please keep your node running during this cooldown period.",
)
}
var render string
- if m.State.Status.Network == "testnet-v1.0" || m.State.Status.Network == "mainnet-v1.0" {
+ if m.State.Status.Network == "testnet-v1.0" {
render = lipgloss.JoinVertical(
lipgloss.Center,
intro,
diff --git a/ui/pages/accounts/accounts_test.go b/ui/pages/accounts/accounts_test.go
index 5866a145..0172b01a 100644
--- a/ui/pages/accounts/accounts_test.go
+++ b/ui/pages/accounts/accounts_test.go
@@ -2,8 +2,8 @@ package accounts
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
@@ -13,7 +13,7 @@ import (
)
func Test_New(t *testing.T) {
- m := New(&internal.StateModel{})
+ m := New(&algod.StateModel{})
acc := m.SelectedAccount()
if acc != nil {
@@ -42,9 +42,9 @@ func Test_New(t *testing.T) {
}
// Update syncing state
- m.Data.Status.State = internal.SyncingState
+ m.Data.Status.State = algod.SyncingState
m.makeRows()
- if m.Data.Status.State != internal.SyncingState {
+ if m.Data.Status.State != algod.SyncingState {
}
}
diff --git a/ui/pages/accounts/controller.go b/ui/pages/accounts/controller.go
index e879be29..27f91f5b 100644
--- a/ui/pages/accounts/controller.go
+++ b/ui/pages/accounts/controller.go
@@ -1,9 +1,9 @@
package accounts
import (
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@@ -18,8 +18,8 @@ func (m ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) {
switch msg := msg.(type) {
- case internal.StateModel:
- m.Data = &msg
+ case *algod.StateModel:
+ m.Data = msg
m.table.SetRows(*m.makeRows())
case tea.KeyMsg:
switch msg.String() {
diff --git a/ui/pages/accounts/model.go b/ui/pages/accounts/model.go
index c565a130..d1fa3bf1 100644
--- a/ui/pages/accounts/model.go
+++ b/ui/pages/accounts/model.go
@@ -1,18 +1,18 @@
package accounts
import (
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
"sort"
"strconv"
"time"
- "github.com/algorandfoundation/algorun-tui/internal"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
)
type ViewModel struct {
- Data *internal.StateModel
+ Data *algod.StateModel
Title string
Navigation string
@@ -24,7 +24,7 @@ type ViewModel struct {
table table.Model
}
-func New(state *internal.StateModel) ViewModel {
+func New(state *algod.StateModel) ViewModel {
m := ViewModel{
Title: "Accounts",
Width: 0,
@@ -54,8 +54,8 @@ func New(state *internal.StateModel) ViewModel {
return m
}
-func (m ViewModel) SelectedAccount() *internal.Account {
- var account *internal.Account
+func (m ViewModel) SelectedAccount() *algod.Account {
+ var account *algod.Account
var selectedRow = m.table.SelectedRow()
if selectedRow != nil {
selectedAccount := m.Data.Accounts[selectedRow[0]]
@@ -95,7 +95,7 @@ func (m ViewModel) makeRows() *[]table.Row {
}
// Override the state while syncing
- if m.Data.Status.State != internal.StableState {
+ if m.Data.Status.State != algod.StableState {
expires = "SYNCING"
}
diff --git a/ui/pages/accounts/view.go b/ui/pages/accounts/view.go
index 0e77098d..2d9d038e 100644
--- a/ui/pages/accounts/view.go
+++ b/ui/pages/accounts/view.go
@@ -1,7 +1,7 @@
package accounts
import (
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/style"
)
func (m ViewModel) View() string {
diff --git a/ui/pages/keys/controller.go b/ui/pages/keys/controller.go
index 1a1f2e6c..7dab673b 100644
--- a/ui/pages/keys/controller.go
+++ b/ui/pages/keys/controller.go
@@ -1,9 +1,10 @@
package keys
import (
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@@ -19,7 +20,7 @@ func (m ViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) {
switch msg := msg.(type) {
// When the State changes
- case internal.StateModel:
+ case *algod.StateModel:
m.Data = msg.ParticipationKeys
m.table.SetRows(*m.makeRows(m.Data))
m.Participation = msg.Accounts[m.Address].Participation
@@ -30,7 +31,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (ViewModel, tea.Cmd) {
m.table.SetRows(*m.makeRows(m.Data))
// When a confirmation Modal is finished deleting
case app.DeleteFinished:
- internal.RemovePartKeyByID(m.Data, msg.Id)
+ participation.RemovePartKeyByID(&m.Data, msg.Id)
m.table.SetRows(*m.makeRows(m.Data))
// When the user interacts with the render
case tea.KeyMsg:
diff --git a/ui/pages/keys/keys_test.go b/ui/pages/keys/keys_test.go
index 4c25a6b2..e0eca968 100644
--- a/ui/pages/keys/keys_test.go
+++ b/ui/pages/keys/keys_test.go
@@ -2,10 +2,10 @@ package keys
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal/test/mock"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/test/mock"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/internal/test"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
@@ -30,7 +30,7 @@ func Test_New(t *testing.T) {
if cmd != nil {
t.Errorf("Expected no commands")
}
- m.Data = &mock.Keys
+ m.Data = mock.Keys
m, _ = m.HandleMessage(app.AccountSelected{Address: "ABC", Participation: &api.AccountParticipation{
SelectionParticipationKey: nil,
StateProofKey: nil,
@@ -54,7 +54,7 @@ func Test_New(t *testing.T) {
func Test_Snapshot(t *testing.T) {
t.Run("Visible", func(t *testing.T) {
- model := New("ABC", &mock.Keys)
+ model := New("ABC", mock.Keys)
model, _ = model.HandleMessage(tea.WindowSizeMsg{Width: 80, Height: 40})
got := ansi.Strip(model.View())
golden.RequireEqual(t, []byte(got))
@@ -64,7 +64,7 @@ func Test_Snapshot(t *testing.T) {
func Test_Messages(t *testing.T) {
// Create the Model
- m := New("ABC", &mock.Keys)
+ m := New("ABC", mock.Keys)
//m, _ = m.Address = "ABC"
tm := teatest.NewTestModel(
t, m,
diff --git a/ui/pages/keys/model.go b/ui/pages/keys/model.go
index b95bb28e..27e0a8a2 100644
--- a/ui/pages/keys/model.go
+++ b/ui/pages/keys/model.go
@@ -1,13 +1,13 @@
package keys
import (
- "github.com/algorandfoundation/algorun-tui/internal"
+ "github.com/algorandfoundation/nodekit/internal/algod/participation"
"sort"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/style"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/ui/utils"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/ui/utils"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
)
@@ -20,7 +20,7 @@ type ViewModel struct {
Participation *api.AccountParticipation
// Data holds a pointer to a slice of ParticipationKey, representing the set of participation keys managed by the ViewModel.
- Data *[]api.ParticipationKey
+ Data participation.List
// Title represents the title displayed at the top of the ViewModel's UI.
Title string
@@ -40,7 +40,7 @@ type ViewModel struct {
}
// New initializes and returns a new ViewModel for managing participation keys.
-func New(address string, keys *[]api.ParticipationKey) ViewModel {
+func New(address string, keys participation.List) ViewModel {
m := ViewModel{
// State
Address: address,
@@ -93,7 +93,7 @@ func (m ViewModel) SelectedKey() (*api.ParticipationKey, bool) {
var partkey *api.ParticipationKey
var active bool
selected := m.table.SelectedRow()
- for _, key := range *m.Data {
+ for _, key := range m.Data {
if len(selected) > 0 && key.Id == selected[0] {
partkey = &key
active = selected[2] == "YES"
@@ -119,7 +119,7 @@ func (m ViewModel) makeColumns(width int) []table.Column {
// makeRows processes a slice of ParticipationKeys and returns a sorted slice of table rows
// filtered by the ViewModel's address.
-func (m ViewModel) makeRows(keys *[]api.ParticipationKey) *[]table.Row {
+func (m ViewModel) makeRows(keys participation.List) *[]table.Row {
rows := make([]table.Row, 0)
if keys == nil || m.Address == "" {
return &rows
@@ -127,9 +127,9 @@ func (m ViewModel) makeRows(keys *[]api.ParticipationKey) *[]table.Row {
var activeId *string
if m.Participation != nil {
- activeId = internal.FindParticipationIdForVoteKey(keys, m.Participation.VoteParticipationKey)
+ activeId = participation.FindParticipationIdForVoteKey(keys, m.Participation.VoteParticipationKey)
}
- for _, key := range *keys {
+ for _, key := range keys {
if key.Address == m.Address {
isActive := "N/A"
if activeId != nil && *activeId == key.Id {
diff --git a/ui/pages/keys/view.go b/ui/pages/keys/view.go
index b60ce8b4..8f40f733 100644
--- a/ui/pages/keys/view.go
+++ b/ui/pages/keys/view.go
@@ -1,7 +1,7 @@
package keys
import (
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/ui/style"
)
func (m ViewModel) View() string {
diff --git a/ui/protocol.go b/ui/protocol.go
index a9e91e59..e2ef0950 100644
--- a/ui/protocol.go
+++ b/ui/protocol.go
@@ -1,17 +1,17 @@
package ui
import (
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"strconv"
"strings"
)
-// ProtocolViewModel includes the internal.StatusModel and internal.MetricsModel
+// ProtocolViewModel includes the internal.StatusModel and internal.Model
type ProtocolViewModel struct {
- Data internal.StatusModel
+ Data algod.Status
TerminalWidth int
TerminalHeight int
IsVisible bool
@@ -32,7 +32,7 @@ func (m ProtocolViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m ProtocolViewModel) HandleMessage(msg tea.Msg) (ProtocolViewModel, tea.Cmd) {
switch msg := msg.(type) {
// Handle a Status Update
- case internal.StatusModel:
+ case algod.Status:
m.Data = msg
return m, nil
// Update Viewport Size
@@ -96,7 +96,7 @@ func (m ProtocolViewModel) View() string {
}
// MakeProtocolViewModel constructs a ProtocolViewModel using a given StatusModel and predefined metrics.
-func MakeProtocolViewModel(state *internal.StateModel) ProtocolViewModel {
+func MakeProtocolViewModel(state *algod.StateModel) ProtocolViewModel {
return ProtocolViewModel{
Data: state.Status,
TerminalWidth: 0,
diff --git a/ui/protocol_test.go b/ui/protocol_test.go
index a207be37..fd53625a 100644
--- a/ui/protocol_test.go
+++ b/ui/protocol_test.go
@@ -2,7 +2,7 @@ package ui
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal"
+ "github.com/algorandfoundation/nodekit/internal/algod"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/exp/golden"
@@ -13,8 +13,8 @@ import (
var protocolViewSnapshots = map[string]ProtocolViewModel{
"Hidden": {
- Data: internal.StatusModel{
- State: "SYNCING",
+ Data: algod.Status{
+ State: algod.SyncingState,
Version: "v0.0.0-test",
Network: "test-v1",
Voting: true,
@@ -26,8 +26,8 @@ var protocolViewSnapshots = map[string]ProtocolViewModel{
IsVisible: false,
},
"HiddenHeight": {
- Data: internal.StatusModel{
- State: "SYNCING",
+ Data: algod.Status{
+ State: algod.SyncingState,
Version: "v0.0.0-test",
Network: "test-v1",
Voting: true,
@@ -39,8 +39,8 @@ var protocolViewSnapshots = map[string]ProtocolViewModel{
IsVisible: true,
},
"Visible": {
- Data: internal.StatusModel{
- State: "SYNCING",
+ Data: algod.Status{
+ State: algod.SyncingState,
Version: "v0.0.0-test",
Network: "test-v1",
Voting: true,
@@ -52,8 +52,8 @@ var protocolViewSnapshots = map[string]ProtocolViewModel{
IsVisible: true,
},
"VisibleSmall": {
- Data: internal.StatusModel{
- State: "SYNCING",
+ Data: algod.Status{
+ State: algod.SyncingState,
Version: "v0.0.0-test",
Network: "test-v1",
Voting: true,
@@ -65,8 +65,8 @@ var protocolViewSnapshots = map[string]ProtocolViewModel{
IsVisible: true,
},
"NoVoteOrUpgrade": {
- Data: internal.StatusModel{
- State: "SYNCING",
+ Data: algod.Status{
+ State: algod.SyncingState,
Version: "v0.0.0-test",
Network: "test-v1",
Voting: false,
@@ -78,8 +78,8 @@ var protocolViewSnapshots = map[string]ProtocolViewModel{
IsVisible: true,
},
"NoVoteOrUpgradeSmall": {
- Data: internal.StatusModel{
- State: "SYNCING",
+ Data: algod.Status{
+ State: algod.SyncingState,
Version: "v0.0.0-test",
Network: "test-v1",
Voting: false,
@@ -103,13 +103,13 @@ func Test_ProtocolSnapshot(t *testing.T) {
// Test_ProtocolMessages handles any additional tests like sending messages
func Test_ProtocolMessages(t *testing.T) {
- state := internal.StateModel{
- Status: internal.StatusModel{
+ state := algod.StateModel{
+ Status: algod.Status{
LastRound: 1337,
NeedsUpdate: true,
- State: internal.SyncingState,
+ State: algod.SyncingState,
},
- Metrics: internal.MetricsModel{
+ Metrics: algod.Metrics{
RoundTime: 0,
TX: 0,
RX: 0,
@@ -134,7 +134,7 @@ func Test_ProtocolMessages(t *testing.T) {
teatest.WithCheckInterval(time.Millisecond*100),
teatest.WithDuration(time.Second*3),
)
- tm.Send(internal.StatusModel{
+ tm.Send(algod.Status{
State: "",
Version: "",
Network: "",
diff --git a/ui/status.go b/ui/status.go
index 63eb60fd..a7a4d822 100644
--- a/ui/status.go
+++ b/ui/status.go
@@ -2,8 +2,8 @@ package ui
import (
"fmt"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/style"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"math"
@@ -14,7 +14,7 @@ import (
// StatusViewModel is extended from the internal.StatusModel
type StatusViewModel struct {
- Data *internal.StateModel
+ Data *algod.StateModel
TerminalWidth int
TerminalHeight int
IsVisible bool
@@ -34,8 +34,8 @@ func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m StatusViewModel) HandleMessage(msg tea.Msg) (StatusViewModel, tea.Cmd) {
switch msg := msg.(type) {
// Is it a heartbeat of the latest round?
- case internal.StateModel:
- m.Data = &msg
+ case *algod.StateModel:
+ m.Data = msg
// Is it a resize event?
case tea.WindowSizeMsg:
m.TerminalWidth = msg.Width
@@ -83,7 +83,7 @@ func (m StatusViewModel) View() string {
var end string
switch m.Data.Status.State {
- case internal.StableState:
+ case algod.StableState:
end = style.Green.Render(strings.ToUpper(string(m.Data.Status.State))) + " "
default:
end = style.Yellow.Render(strings.ToUpper(string(m.Data.Status.State))) + " "
@@ -94,7 +94,7 @@ func (m StatusViewModel) View() string {
row1 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
roundTime := fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second))
- if m.Data.Status.State != internal.StableState {
+ if m.Data.Status.State != algod.StableState {
roundTime = "--"
}
beginning = style.Blue.Render(" Round time: ") + roundTime
@@ -104,7 +104,7 @@ func (m StatusViewModel) View() string {
row2 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
tps := fmt.Sprintf("%.2f", m.Data.Metrics.TPS)
- if m.Data.Status.State != internal.StableState {
+ if m.Data.Status.State != algod.StableState {
tps = "--"
}
beginning = style.Blue.Render(" TPS: ") + tps
@@ -124,7 +124,7 @@ func (m StatusViewModel) View() string {
}
// MakeStatusViewModel constructs the model to be used in a tea.Program
-func MakeStatusViewModel(state *internal.StateModel) StatusViewModel {
+func MakeStatusViewModel(state *algod.StateModel) StatusViewModel {
// Create the Model
m := StatusViewModel{
Data: state,
diff --git a/ui/status_test.go b/ui/status_test.go
index fc10d856..2822507b 100644
--- a/ui/status_test.go
+++ b/ui/status_test.go
@@ -2,7 +2,7 @@ package ui
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal"
+ "github.com/algorandfoundation/nodekit/internal/algod"
"testing"
"time"
@@ -14,13 +14,13 @@ import (
var statusViewSnapshots = map[string]StatusViewModel{
"Syncing": {
- Data: &internal.StateModel{
- Status: internal.StatusModel{
+ Data: &algod.StateModel{
+ Status: algod.Status{
LastRound: 1337,
NeedsUpdate: true,
- State: "SYNCING",
+ State: algod.SyncingState,
},
- Metrics: internal.MetricsModel{
+ Metrics: algod.Metrics{
RoundTime: 0,
TX: 0,
},
@@ -30,13 +30,13 @@ var statusViewSnapshots = map[string]StatusViewModel{
IsVisible: true,
},
"Hidden": {
- Data: &internal.StateModel{
- Status: internal.StatusModel{
+ Data: &algod.StateModel{
+ Status: algod.Status{
LastRound: 1337,
NeedsUpdate: true,
- State: "SYNCING",
+ State: algod.SyncingState,
},
- Metrics: internal.MetricsModel{
+ Metrics: algod.Metrics{
RoundTime: 0,
TX: 0,
},
@@ -46,13 +46,13 @@ var statusViewSnapshots = map[string]StatusViewModel{
IsVisible: false,
},
"Loading": {
- Data: &internal.StateModel{
- Status: internal.StatusModel{
+ Data: &algod.StateModel{
+ Status: algod.Status{
LastRound: 1337,
NeedsUpdate: true,
- State: "SYNCING",
+ State: algod.SyncingState,
},
- Metrics: internal.MetricsModel{
+ Metrics: algod.Metrics{
RoundTime: 0,
TX: 0,
},
@@ -73,13 +73,13 @@ func Test_StatusSnapshot(t *testing.T) {
}
func Test_StatusMessages(t *testing.T) {
- state := internal.StateModel{
- Status: internal.StatusModel{
+ state := algod.StateModel{
+ Status: algod.Status{
LastRound: 1337,
NeedsUpdate: true,
- State: internal.SyncingState,
+ State: algod.SyncingState,
},
- Metrics: internal.MetricsModel{
+ Metrics: algod.Metrics{
RoundTime: 0,
TX: 0,
RX: 0,
diff --git a/ui/style/style.go b/ui/style/style.go
index 94c1089c..a609666b 100644
--- a/ui/style/style.go
+++ b/ui/style/style.go
@@ -19,11 +19,12 @@ var (
Height(height).
BorderForeground(lipgloss.Color(color))
}
-
- Blue = func() lipgloss.Style {
- return lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
+ MarketingBlue = lipgloss.NewStyle().Foreground(lipgloss.Color("#2d2df1")).Render
+ MarketingTeal = lipgloss.NewStyle().Foreground(lipgloss.Color("#17CAC6")).Render
+ Blue = func() lipgloss.Style {
+ return lipgloss.NewStyle().Foreground(lipgloss.Color("#12"))
}()
- Cyan = lipgloss.NewStyle().Foreground(lipgloss.Color("14"))
+ Cyan = lipgloss.NewStyle().Foreground(lipgloss.Color("#14"))
Yellow = lipgloss.NewStyle().Foreground(lipgloss.Color("11"))
Green = lipgloss.NewStyle().Foreground(lipgloss.Color("10"))
Red = lipgloss.NewStyle().Foreground(lipgloss.Color("9"))
@@ -178,3 +179,28 @@ func TruncateLeft(line string, padding int) string {
return ansiStyle + strings.Join(wrapped[1:], "")
}
+
+const BANNER_NODE = `
+โโโ โโ โโโโโโ โโโโโโ โโโโโโโ
+โโโโ โโ โโ โโ โโ โโ โโ
+โโ โโ โโ โโ โโ โโ โโ โโโโโ
+โโ โโ โโ โโ โโ โโ โโ โโ
+โโ โโโโ โโโโโโ โโโโโโ โโโโโโโ `
+const BANNER_KIT = `
+โโโ โโโโโโโโโโโโโโโ
+โโโ โโโโโโโโโโโโโโโโ
+โโโโโโโ โโโ โโโ
+โโโโโโโ โโโ โโโ
+โโโ โโโโโโ โโโ
+โโโ โโโโโโ โโโ`
+
+var BANNER string
+
+func init() {
+ var res string
+ lines := strings.Split(BANNER_KIT, "\n")
+ for i, line := range strings.Split(BANNER_NODE, "\n") {
+ res += MarketingBlue(line) + MarketingTeal(lines[i]) + "\n"
+ }
+ BANNER = res
+}
diff --git a/ui/viewport.go b/ui/viewport.go
index dad291f9..0c4c6d20 100644
--- a/ui/viewport.go
+++ b/ui/viewport.go
@@ -3,12 +3,12 @@ package ui
import (
"errors"
"fmt"
- "github.com/algorandfoundation/algorun-tui/api"
- "github.com/algorandfoundation/algorun-tui/internal"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- "github.com/algorandfoundation/algorun-tui/ui/modal"
- "github.com/algorandfoundation/algorun-tui/ui/pages/accounts"
- "github.com/algorandfoundation/algorun-tui/ui/pages/keys"
+ "github.com/algorandfoundation/nodekit/api"
+ "github.com/algorandfoundation/nodekit/internal/algod"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ "github.com/algorandfoundation/nodekit/ui/modal"
+ "github.com/algorandfoundation/nodekit/ui/pages/accounts"
+ "github.com/algorandfoundation/nodekit/ui/pages/keys"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@@ -18,7 +18,7 @@ type ViewportViewModel struct {
PageWidth, PageHeight int
TerminalWidth, TerminalHeight int
- Data *internal.StateModel
+ Data *algod.StateModel
// Header Components
status StatusViewModel
@@ -28,9 +28,10 @@ type ViewportViewModel struct {
accountsPage accounts.ViewModel
keysPage keys.ViewModel
- modal *modal.ViewModel
- page app.Page
- client api.ClientWithResponsesInterface
+ outside app.Outside
+ modal *modal.ViewModel
+ page app.Page
+ client api.ClientWithResponsesInterface
}
// Init is a no-op
@@ -57,8 +58,8 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.page = msg
// When the state updates
- case internal.StateModel:
- m.Data = &msg
+ case *algod.StateModel:
+ m.Data = msg
m.accountsPage, cmd = m.accountsPage.HandleMessage(msg)
cmds = append(cmds, cmd)
m.keysPage, cmd = m.keysPage.HandleMessage(msg)
@@ -75,7 +76,7 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "g":
// Only open modal when it is closed and not syncing
- if !m.modal.Open && m.Data.Status.State == internal.StableState && m.Data.Metrics.RoundTime > 0 {
+ if !m.modal.Open && m.Data.Status.State == algod.StableState && m.Data.Metrics.RoundTime > 0 {
address := ""
selected := m.accountsPage.SelectedAccount()
if selected != nil {
@@ -86,7 +87,7 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Address: address,
Type: app.GenerateModal,
})
- } else if m.Data.Status.State != internal.StableState || m.Data.Metrics.RoundTime == 0 {
+ } else if m.Data.Status.State != algod.StableState || m.Data.Metrics.RoundTime == 0 {
genErr := errors.New("Please wait for more data to sync before generating a key")
m.modal, cmd = m.modal.HandleMessage(genErr)
cmds = append(cmds, cmd)
@@ -116,8 +117,7 @@ func (m ViewportViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
return m, nil
- case "ctrl+c":
- case "q":
+ case "q", "ctrl+c":
// Close the app when anything other than generate modal is visible
if !m.modal.Open || (m.modal.Open && m.modal.Type != app.GenerateModal) {
return m, tea.Quit
@@ -211,7 +211,7 @@ func (m ViewportViewModel) headerView() string {
}
// NewViewportViewModel handles the construction of the TUI viewport
-func NewViewportViewModel(state *internal.StateModel, client api.ClientWithResponsesInterface) (*ViewportViewModel, error) {
+func NewViewportViewModel(state *algod.StateModel, client api.ClientWithResponsesInterface) (*ViewportViewModel, error) {
m := ViewportViewModel{
Data: state,
@@ -224,8 +224,8 @@ func NewViewportViewModel(state *internal.StateModel, client api.ClientWithRespo
keysPage: keys.New("", state.ParticipationKeys),
// Modal
- modal: modal.New("", false, state),
-
+ modal: modal.New("", false, state),
+ outside: app.NewOutside(),
// Current Page
page: app.AccountsPage,
// RPC client
diff --git a/ui/viewport_test.go b/ui/viewport_test.go
index dc606a32..fe068bbb 100644
--- a/ui/viewport_test.go
+++ b/ui/viewport_test.go
@@ -2,9 +2,9 @@ package ui
import (
"bytes"
- "github.com/algorandfoundation/algorun-tui/internal/test"
- "github.com/algorandfoundation/algorun-tui/ui/app"
- uitest "github.com/algorandfoundation/algorun-tui/ui/internal/test"
+ "github.com/algorandfoundation/nodekit/internal/test"
+ "github.com/algorandfoundation/nodekit/ui/app"
+ uitest "github.com/algorandfoundation/nodekit/ui/internal/test"
"testing"
"time"