diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..5cb632d
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,90 @@
+name: Main pipeline
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ release:
+ types: [ published ]
+
+permissions:
+ contents: write
+
+jobs:
+ frontend:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v3
+ with:
+ version: 10
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ cache: 'pnpm'
+ cache-dependency-path: frontend/pnpm-lock.yaml
+
+ - name: Install Dependencies
+ run: pnpm install -C frontend --frozen-lockfile
+
+ - name: Typecheck
+ working-directory: frontend
+ run: pnpm typecheck
+
+ - name: Lint
+ working-directory: frontend
+ run: pnpm lint
+
+ - name: Test
+ working-directory: frontend
+ run: pnpm test
+
+ - name: Build Frontend
+ run: make ui
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: 'go.mod'
+
+ - name: Test Backend
+ run: make test
+
+ - name: Build Go Binaries
+ run: |
+ # os/arch/extension
+ TARGETS=("linux/amd64/" "linux/arm64/" "darwin/amd64/" "darwin/arm64/" "windows/amd64/.exe")
+
+ for target in "${TARGETS[@]}"; do
+ IFS="/" read -r OS ARCH EXT <<< "$target"
+
+ echo "Building for $OS-$ARCH..."
+ PLATFORM_DIR="dist/${OS}_${ARCH}"
+ mkdir -p "$PLATFORM_DIR"
+
+ for d in cmd/*/; do
+ binary_name=$(basename "$d")
+ GOOS=$OS GOARCH=$ARCH go build -o "${PLATFORM_DIR}/${binary_name}${EXT}" "./$d"
+ done
+
+ VERSION=${{ github.event.release.tag_name || 'dev' }}
+ if [ "$OS" == "windows" ]; then
+ cd dist && zip -r "../codesearch-${VERSION}-${OS}-${ARCH}.zip" "${OS}_${ARCH}" && cd ..
+ else
+ tar -cvzf "codesearch-${VERSION}-${OS}-${ARCH}.tar.gz" -C dist "${OS}_${ARCH}"
+ fi
+ done
+
+ - name: Upload to Release
+ if: github.event_name == 'release'
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ *.tar.gz
+ *.zip
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..61516bc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.idea/
+
+/cmd/cserver/static/
+/db/
+/config
+/Makefile
diff --git a/AUTHORS b/AUTHORS
deleted file mode 100644
index d7fda85..0000000
--- a/AUTHORS
+++ /dev/null
@@ -1,4 +0,0 @@
-# This source code is copyright "The Go Authors",
-# as defined by the AUTHORS file in the root of the Go tree.
-#
-# http://tip.golang.org/AUTHORS.
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
deleted file mode 100644
index 4546fce..0000000
--- a/CONTRIBUTORS
+++ /dev/null
@@ -1,5 +0,0 @@
-# The official list of people who can contribute code to the repository
-# is maintained in the standard Go repository as the CONTRIBUTORS
-# file in the root of the Go tree.
-#
-# http://tip.golang.org/CONTRIBUTORS
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..131177f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+all: ui go
+all-restart: all
+ systemctl --user restart codesearch-server.service
+
+go:
+ go install ./...
+
+test:
+ go test ./...
+
+ui:
+ cd frontend && pnpm install && pnpm build --emptyOutDir --outDir ../cmd/cserver/static
+
+update:
+ ~/.go/bin/csupdater --config ./config
diff --git a/README b/README
deleted file mode 100644
index 65e4141..0000000
--- a/README
+++ /dev/null
@@ -1,16 +0,0 @@
-Code Search is a tool for indexing and then performing
-regular expression searches over large bodies of source code.
-It is a set of command-line programs written in Go.
-
-For background and an overview of the commands,
-see http://swtch.com/~rsc/regexp/regexp4.html.
-
-To install:
-
- go get github.com/google/codesearch/cmd/...
-
-Use "go get -u" to update an existing installation.
-
-Russ Cox
-rsc@swtch.com
-June 2015
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c2d5bfd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,111 @@
+# Code search
+
+Extends [github.com/google/codesearch](https://github.com/google/codesearch) with a tool to sync git repositories
+to be indexed and a web interface. The sync tool and web interface inspired by [github.com/hakonhall/codesearch](https://github.com/hakonhall/codesearch).
+
+## Configuration
+
+Configuration is done via a config file. The format is as follows:
+
+- The file consists of global settings and one or more `[server NAME]` sections.
+- Relative paths are resolved relative to the config file location.
+
+**Global settings:**
+- `code`: Directory for checked-out and indexed source code. Default: `[workdir]/code`
+- `fileindex`: Path to the file index file. Default: `[workdir]/csearch.fileindex`
+- `index`: Path to the code index file. Default: `[workdir]/csearch.index`
+- `port`: Port for the server. Default: `80`
+- `manifest`: Path to the manifest file. Default: `[workdir]/manifest.json`
+- `workdir`: Working directory managed by the program. Required.
+
+**Server section (`[server NAME]`):**
+- `api`: GitHub REST API URL. Default: `https://api.github.com`
+- `exclude`: Regex to exclude repositories (at most one).
+- `include`: Repository or owner to include. Formats:
+ - `OWNER` (all repos for user/org)
+ - `OWNER/REPO` (specific repo)
+ - `OWNER/REPO#BRANCH` (specific branch)
+ - `OWNER/REPO#REF` (specific commit)
+- `token`: OAuth2 token (e.g., personal access token).
+- `weburl`: Git web interface URL. Default: `https://github.com`
+- `url`: Base URL for cloning (e.g., `git@github.com`, `https://github.com`). Required.
+
+**Example config:**
+```ini
+[global]
+code = db
+index = /home/user/.csearchindex
+port = 8080
+
+[server github]
+exclude = ^DEPRECATED
+include = freva
+include = torvalds/linux
+
+[server internal]
+api = https://git.example.com/api
+include = internalorg
+token = ghp_YYYYYYYYYYYYYYYYYYYY
+weburl = https://git.example.com
+url = https://ghp_YYYYYYYYYYYYYYYYYYYY@git.example.com
+```
+
+## Binaries
+
+The following binaries are produced:
+- `cgrep`: Command-line code search using the index. Usage:
+ ```sh
+ cgrep [flags] regexp [file...]
+ ```
+ Flags: `-c` (count), `-h` (no filename), `-i` (case-insensitive), `-l` (list files), `-n` (line numbers), `-v` (invert match)
+
+- `cindex`: Builds the code search index. Usage:
+ ```sh
+ cindex [flags] [path...]
+ ```
+ Flags: `-index` (index file), `-list` (list indexed paths), `-reset` (discard existing index), `-zip` (index zip files)
+
+- `csearch`: Behaves like `cgrep`, but over all indexed files with option to limit search to files matching a regex. Usage:
+ ```sh
+ csearch [flags] regexp
+ ```
+ Flags: `-c` (count), `-f` (file regexp), `-h` (no filename), `-i` (case-insensitive), `-l` (list files), `-n` (line numbers), `-index` (index file)
+
+- `cserver`: HTTP server providing the web UI. Usage:
+ ```sh
+ cserver --config path/to/config
+ ```
+- `csupdater`: Updates repositories and indices from remote sources. Usage:
+ ```sh
+ csupdater --config path/to/config [--manifest] [--sync] [--index] [--verbose] [--exit-early] [--help-config]
+ ```
+ Flags: `--config` (config file), `--exit-early` (exit without updating index if no change in sync), `--manifest` (whether to update manifest), `--sync` (whether to sync repositories), `--index` (whether to update index)
+ By default, runs all update steps.
+
+## Building
+
+Requirements: [Go](https://golang.org/doc/install) (>=1.23), [pnpm](https://pnpm.io/) (>=10).
+
+Run:
+```sh
+make
+```
+This installs Go binaries and builds the frontend UI.
+
+## Automatic Updates and Server
+
+To run the server and keep the index up to date automatically, install the provided systemd user services:
+
+1. Copy the service files from `systemd/` to your user systemd directory:
+ ```sh
+ cp systemd/codesearch-server.service systemd/codesearch-updater.service ~/.config/systemd/user/
+ ```
+ then edit them to set the correct paths for the binaries and config file.
+2. Reload systemd and enable the services:
+ ```sh
+ systemctl --user daemon-reload
+ systemctl --user enable --now codesearch-server.service codesearch-updater.service
+ ```
+
+- `codesearch-server.service` runs the HTTP server (`cserver`).
+- `codesearch-updater.service` keeps the repositories and index up to date (`csupdater`).
diff --git a/cmd/cgrep/cgrep.go b/cmd/cgrep/cgrep.go
index 5e7404f..04a5bb6 100644
--- a/cmd/cgrep/cgrep.go
+++ b/cmd/cgrep/cgrep.go
@@ -11,7 +11,7 @@ import (
"os"
"runtime/pprof"
- "github.com/google/codesearch/regexp"
+ "github.com/freva/codesearch/regexp"
)
var usageMessage = `usage: cgrep [-c] [-h] [-i] [-l] [-n] [-v] regexp [file...]
@@ -24,7 +24,7 @@ cannot be abbreviated to -in.
`
func usage() {
- fmt.Fprintf(os.Stderr, usageMessage)
+ fmt.Fprint(os.Stderr, usageMessage)
os.Exit(2)
}
diff --git a/cmd/cindex/cindex.go b/cmd/cindex/cindex.go
index d82bf6c..756619e 100644
--- a/cmd/cindex/cindex.go
+++ b/cmd/cindex/cindex.go
@@ -13,13 +13,13 @@ import (
"runtime/pprof"
"slices"
- "github.com/google/codesearch/index"
+ "github.com/freva/codesearch/index"
)
-var usageMessage = `usage: cindex [-list] [-reset] [-zip] [path...]
+var usageMessage = `usage: cindex [-index file] [-list] [-reset] [-zip] [path...]
Cindex prepares the trigram index for use by csearch. The index is the
-file named by $CSEARCHINDEX, or else $HOME/.csearchindex.
+file named by -index, or else $CSEARCHINDEX, or else $HOME/.csearchindex.
The simplest invocation is
@@ -53,11 +53,12 @@ With no path arguments, cindex -reset removes the index.
`
func usage() {
- fmt.Fprintf(os.Stderr, usageMessage)
+ fmt.Fprint(os.Stderr, usageMessage)
os.Exit(2)
}
var (
+ indexFlag = flag.String("index", "", "path to index file")
listFlag = flag.Bool("list", false, "list indexed paths and exit")
resetFlag = flag.Bool("reset", false, "discard existing index")
verboseFlag = flag.Bool("verbose", false, "print extra information")
@@ -72,8 +73,10 @@ func main() {
flag.Usage = usage
flag.Parse()
+ indexpath := index.File(*indexFlag)
+
if *listFlag {
- ix := index.Open(index.File())
+ ix := index.Open(index.File(indexpath))
if *checkFlag {
if err := ix.Check(); err != nil {
log.Fatal(err)
@@ -96,12 +99,12 @@ func main() {
}
if *resetFlag && flag.NArg() == 0 {
- os.Remove(index.File())
+ os.Remove(index.File(indexpath))
return
}
var roots []index.Path
if flag.NArg() == 0 {
- ix := index.Open(index.File())
+ ix := index.Open(index.File(indexpath))
roots = slices.Collect(ix.Roots().All())
} else {
// Translate arguments to absolute paths so that
@@ -117,7 +120,7 @@ func main() {
slices.SortFunc(roots, index.Path.Compare)
}
- master := index.File()
+ master := index.File(indexpath)
if _, err := os.Stat(master); err != nil {
// Does not exist.
*resetFlag = true
diff --git a/cmd/csearch/csearch.go b/cmd/csearch/csearch.go
index 58af5b8..ed37d3f 100644
--- a/cmd/csearch/csearch.go
+++ b/cmd/csearch/csearch.go
@@ -14,8 +14,8 @@ import (
"runtime/pprof"
"strings"
- "github.com/google/codesearch/index"
- "github.com/google/codesearch/regexp"
+ "github.com/freva/codesearch/index"
+ "github.com/freva/codesearch/regexp"
)
var usageMessage = `usage: csearch [-c] [-f fileregexp] [-h] [-i] [-l] [-n] regexp
@@ -44,7 +44,7 @@ empty, $HOME/.csearchindex.
`
func usage() {
- fmt.Fprintf(os.Stderr, usageMessage)
+ fmt.Fprint(os.Stderr, usageMessage)
os.Exit(2)
}
@@ -52,6 +52,7 @@ var (
fFlag = flag.String("f", "", "search only files with names matching this regexp")
iFlag = flag.Bool("i", false, "case-insensitive search")
htmlFlag = flag.Bool("html", false, "print HTML output")
+ indexFlag = flag.String("index", "", "path to index file")
verboseFlag = flag.Bool("verbose", false, "print extra information")
bruteFlag = flag.Bool("brute", false, "brute force - search all files in index")
cpuProfile = flag.String("cpuprofile", "", "write cpu profile to this file")
@@ -109,7 +110,7 @@ func Main() {
log.Printf("query: %s\n", q)
}
- ix := index.Open(index.File())
+ ix := index.Open(index.File(*indexFlag))
ix.Verbose = *verboseFlag
var post []int
if *bruteFlag {
diff --git a/cmd/cserver/cserver.go b/cmd/cserver/cserver.go
new file mode 100644
index 0000000..fe108fd
--- /dev/null
+++ b/cmd/cserver/cserver.go
@@ -0,0 +1,88 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "embed"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/fs"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+
+ "github.com/freva/codesearch/internal/config"
+)
+
+var (
+ CodeDir string
+ ManifestPath string
+ CodeIndexPath string
+ FileIndexPath string
+)
+
+//go:embed static
+var embedFS embed.FS
+
+func manifestHandler(w http.ResponseWriter, r *http.Request) {
+ handleError(w, func() error {
+ manifest, err := config.ReadManifest(ManifestPath)
+ if err != nil {
+ return fmt.Errorf("Failed to read manifest: %w", err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ return json.NewEncoder(w).Encode(manifest)
+ })
+}
+
+func main() {
+ var configPath string
+ flag.StringVar(&configPath, "config", "", "Path to config file (required).")
+
+ flag.Usage = func() {
+ _, _ = fmt.Fprintf(os.Stderr, `usage: cserver [OPTION...]
+Start HTTP server, serving a search and view interface of a source tree.`)
+ flag.PrintDefaults()
+ }
+ flag.Parse()
+
+ cfg, err := config.ReadConfig(configPath)
+ if err != nil {
+ log.Fatal("could not parse config file: %w", err)
+ }
+
+ CodeDir = cfg.CodeDir
+ ManifestPath = cfg.ManifestPath
+ CodeIndexPath = cfg.CodeIndexPath
+ FileIndexPath = cfg.FileIndexPath
+ if _, err := os.Stat(CodeIndexPath); err != nil {
+ log.Fatal("Failed to stat code index file: " + CodeIndexPath)
+ }
+
+ staticFS, err := fs.Sub(embedFS, "static")
+ if err != nil {
+ log.Fatal("Failed to resolve embedded static directory: %w", err)
+ }
+
+ fileServer := http.FileServer(http.FS(staticFS))
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if !strings.HasPrefix(r.URL.Path, "/assets/") {
+ r.URL.Path = "/"
+ }
+ fileServer.ServeHTTP(w, r)
+ })
+
+ http.HandleFunc("/rest/manifest", manifestHandler)
+ http.HandleFunc("/rest/file", RestFileHandler)
+ http.HandleFunc("/rest/search", RestSearchHandler)
+ http.HandleFunc("/rest/list", RestListHandler)
+ if err := http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil); err != nil {
+ log.Fatal("ListenAndServe failed: ", err)
+ }
+ fmt.Println("ListenAndServe returned, exiting process!")
+}
diff --git a/cmd/cserver/rest.go b/cmd/cserver/rest.go
new file mode 100644
index 0000000..91ac5ce
--- /dev/null
+++ b/cmd/cserver/rest.go
@@ -0,0 +1,543 @@
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ stdregexp "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "github.com/freva/codesearch/index"
+ "github.com/freva/codesearch/internal/config"
+ "github.com/freva/codesearch/regexp"
+)
+
+var escapedChars = map[rune]string{
+ '"': "\\\"",
+ '\\': "\\\\",
+ '\n': "\\n",
+ '\r': "\\r",
+ '\t': "\\t",
+ '\b': "\\b",
+ '\f': "\\f",
+}
+
+type File struct {
+ Repository *config.Repository
+ Relpath string
+ WebURL string
+}
+
+// path must be relative to the serving directory.
+func resolvePath(manifest *config.Manifest, path string) *File {
+ path = strings.Trim(path, "/")
+ parts := strings.Split(path, "/")
+ if len(parts) >= 3 {
+ prefix := filepath.Join(parts[:3]...)
+ repo, ok := manifest.Repositories[prefix]
+ if ok {
+ return &File{Repository: repo, Relpath: path[min(len(path), len(prefix)+1):], WebURL: manifest.Servers[repo.Server]}
+ }
+ }
+ return nil
+}
+
+func escapeJsonString(str string) string {
+ var result string
+ for _, r := range str {
+ if escaped, ok := escapedChars[r]; ok {
+ result += escaped
+ } else if unicode.IsControl(r) {
+ result += fmt.Sprintf("\\u%04X", r)
+ } else {
+ result += string(r)
+ }
+ }
+ return result
+}
+
+func setHeaders(w http.ResponseWriter) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET")
+}
+
+func handleError(w http.ResponseWriter, f func() error) {
+ if err := f(); err != nil {
+ if len(w.Header()) == 0 {
+ setHeaders(w)
+ w.WriteHeader(http.StatusBadRequest)
+ response := fmt.Sprintf("{\"message\": \"%s\"}", escapeJsonString(err.Error()))
+ if _, wErr := w.Write([]byte(response)); wErr != nil {
+ log.Printf("Failed to write error response: %v. Original error: %v", wErr, err)
+ }
+ } else {
+ log.Println("Error:", err)
+ }
+ }
+}
+
+func maybeWriteComma(w http.ResponseWriter, shouldWriteComma bool) error {
+ var err error
+ if shouldWriteComma {
+ _, err = w.Write([]byte(","))
+ }
+ return err
+}
+
+func writeJsonFileHeader(w http.ResponseWriter, manifest *config.Manifest, path string, pathRegex *stdregexp.Regexp) error {
+ var file = resolvePath(manifest, path)
+ if file == nil {
+ return fmt.Errorf("Failed to resolve path %s", path)
+ }
+
+ if _, err := w.Write([]byte(fmt.Sprintf("{\"path\":\"%s\",\"directory\":\"%s\",\"repository\":\"%s/%s/%s\",\"branch\":\"%s\"",
+ escapeJsonString(file.Relpath), file.Repository.RepoDir(), file.WebURL, file.Repository.Owner, file.Repository.Name, file.Repository.Branch))); err != nil {
+ return err
+ }
+
+ if pathRegex != nil {
+ matches := pathRegex.FindStringSubmatchIndex(path)
+ rangeStr := fmt.Sprintf(",\"range\":[%d,%d]", matches[0], matches[1])
+ if _, err := w.Write([]byte(rangeStr)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func search(w http.ResponseWriter, manifest *config.Manifest, query string, fileFilter string, excludeFileFilter string, maxHits int, ignoreCase bool, beforeLines int, afterLines int) error {
+ // (?m) => ^ and $ match beginning and end of line, respectively
+ queryPattern := "(?m)" + query
+ if ignoreCase {
+ queryPattern = "(?i)" + queryPattern
+ }
+ queryRe, err := regexp.Compile(queryPattern)
+ if err != nil {
+ return fmt.Errorf("Bad query regular expression: %w", err)
+ }
+ queryStdRe, err := stdregexp.Compile(queryPattern)
+ if err != nil {
+ log.Print(err)
+ }
+
+ var fileRe *regexp.Regexp
+ var fileStdRe *stdregexp.Regexp
+ if fileFilter != "" {
+ filePattern := fileFilter
+ if ignoreCase {
+ filePattern = "(?i)" + filePattern
+ }
+
+ fileRe, err = regexp.Compile(filePattern)
+ if err != nil {
+ return fmt.Errorf("Bad file regular expression: %w", err)
+ }
+
+ fileStdRe, err = stdregexp.Compile(filePattern)
+ if err != nil {
+ log.Print(err)
+ }
+ }
+
+ var xFileRe *regexp.Regexp
+ if excludeFileFilter != "" {
+ excludeFilePattern := excludeFileFilter
+ if ignoreCase {
+ excludeFilePattern = "(?i)" + excludeFilePattern
+ }
+
+ xFileRe, err = regexp.Compile(excludeFilePattern)
+ if err != nil {
+ log.Print(err)
+ return fmt.Errorf("Bad exclude file regular expression: %w", err)
+ }
+ }
+
+ q := index.RegexpQuery(queryRe.Syntax)
+ ix := index.Open(CodeIndexPath)
+ ix.Verbose = false
+ var post = ix.PostingQuery(q)
+
+ truncated := false
+ numHits := 0
+
+ setHeaders(w)
+ if _, err := w.Write([]byte("{\"files\":[")); err != nil {
+ return err
+ }
+
+ for _, fileId := range post {
+ if numHits >= maxHits {
+ break
+ }
+
+ fullPath := ix.Name(fileId)
+ path := strings.TrimPrefix(fullPath.String(), CodeDir+"/")
+
+ if fileRe != nil {
+ // Retain only those files matching the file pattern.
+ if fileRe.MatchString(path, true, true) < 0 {
+ continue
+ }
+ }
+
+ if xFileRe != nil {
+ // Skip files matching the exclude file pattern.
+ if xFileRe.MatchString(path, true, true) >= 0 {
+ continue
+ }
+ }
+
+ isFirstHit := true
+ for hit := range regexp.FindMatches(fullPath, queryRe, beforeLines, afterLines) {
+ if isFirstHit {
+ if err := maybeWriteComma(w, numHits > 0); err != nil {
+ return err
+ }
+ if err := writeJsonFileHeader(w, manifest, path, fileStdRe); err != nil {
+ return err
+ }
+ if _, err := w.Write([]byte(",\"lines\":[")); err != nil {
+ return err
+ }
+ isFirstHit = false
+ } else {
+ if err := maybeWriteComma(w, true); err != nil {
+ return err
+ }
+ }
+
+ escapedLine := escapeJsonString(strings.TrimSuffix(hit.Line, "\n"))
+ if _, err := w.Write([]byte(fmt.Sprintf("{\"line\":\"%s\"", escapedLine))); err != nil {
+ return err
+ }
+
+ lineMeta := fmt.Sprintf(",\"number\":%d", hit.Lineno)
+ if hit.Match {
+ matches := queryStdRe.FindStringSubmatchIndex(hit.Line)
+ if matches != nil {
+ lineMeta += fmt.Sprintf(",\"range\":[%d,%d]", matches[0], matches[1])
+ }
+
+ numHits += 1
+ }
+ if _, err := w.Write([]byte(lineMeta + "}")); err != nil {
+ return err
+ }
+
+ if numHits >= maxHits+20 {
+ truncated = true
+ break
+ }
+ }
+
+ if !isFirstHit {
+ if _, err := w.Write([]byte("]}")); err != nil {
+ return err
+ }
+ }
+ }
+
+ _, err = w.Write([]byte(fmt.Sprintf("],\"hits\":%d,\"truncated\":%t,\"matchedFiles\":%d,\"updatedAt\":%d}", numHits, truncated, len(post), manifest.UpdatedAt.UnixMilli())))
+ return err
+}
+
+func searchFile(w http.ResponseWriter, manifest *config.Manifest, fileFilter string, excludeFileFilter string, maxHits int, ignoreCase bool) error {
+ filePattern := "(?m)" + fileFilter
+ if ignoreCase {
+ filePattern = "(?i)" + filePattern
+ }
+ fileRe, err := regexp.Compile(filePattern)
+ if err != nil {
+ return fmt.Errorf("Bad file regular expression: %w", err)
+ }
+
+ // pattern includes e.g. (?i), which is correct even for plain "regexp" package.
+ fileStdRe, err := stdregexp.Compile(filePattern)
+ if err != nil {
+ log.Print(err)
+ fileStdRe = nil
+ }
+
+ var xFileRe *regexp.Regexp
+ if excludeFileFilter != "" {
+ xFilePattern := excludeFileFilter
+ if ignoreCase {
+ xFilePattern = "(?i)" + xFilePattern
+ }
+ xFileRe, err = regexp.Compile(xFilePattern)
+ if err != nil {
+ return fmt.Errorf("Bad exclude file regular expression: %w", err)
+ }
+ }
+
+ idx := index.Open(FileIndexPath)
+ idx.Verbose = false
+ query := index.RegexpQuery(fileRe.Syntax)
+ var post = idx.PostingQuery(query)
+
+ numHits := 0
+ truncated := false
+
+ setHeaders(w)
+ if _, err := w.Write([]byte("{\"files\":[")); err != nil {
+ return err
+ }
+
+ for _, fileId := range post {
+ if numHits >= maxHits {
+ truncated = true
+ break
+ }
+
+ grep := regexp.Grep{Regexp: fileRe, Stderr: os.Stderr}
+ // This is no better than just looping through the lines
+ // of the files and matching (AFAIK), so there's only a
+ // benefit if we don't traverse through all files: Split
+ // up the list of paths in many. Too many => I/O bound.
+ grep.File2(idx.Name(fileId).String())
+
+ for _, hit := range grep.MatchedLines {
+ path := hit.Line
+ if len(path) > 0 && path[len(path)-1] == '\n' {
+ path = path[:len(path)-1]
+ }
+
+ if xFileRe != nil && xFileRe.MatchString(path, true, true) >= 0 {
+ continue
+ }
+
+ if err := maybeWriteComma(w, numHits > 0); err != nil {
+ return err
+ }
+ if err := writeJsonFileHeader(w, manifest, path, fileStdRe); err != nil {
+ return err
+ }
+ if _, err := w.Write([]byte("}")); err != nil {
+ return err
+ }
+
+ numHits += 1
+ if numHits >= maxHits+10 {
+ truncated = true
+ break
+ }
+ }
+ }
+
+ _, err = w.Write([]byte(fmt.Sprintf("],\"hits\":%d,\"truncated\":%t,\"matchedFiles\":%d,\"updatedAt\":%d}", numHits, truncated, len(post), manifest.UpdatedAt.UnixMilli())))
+ return err
+}
+
+func RestSearchHandler(w http.ResponseWriter, r *http.Request) {
+ handleError(w, func() error {
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+
+ query := r.Form.Get("q")
+ fileFilter := r.Form.Get("f")
+ excludeFileFilter := r.Form.Get("xf")
+ ignoreCase := r.Form.Get("i") != ""
+
+ parseNumber := func(param string, defaultValue int) (int, error) {
+ paramValue := r.Form.Get(param)
+ if paramValue == "" {
+ return defaultValue, nil
+ }
+ value, err := strconv.Atoi(paramValue)
+ if err != nil || value < 0 {
+ return -1, fmt.Errorf("Invalid non-negative number for parameter '%s', got '%s'", param, paramValue)
+ }
+ return value, nil
+ }
+ before, err := parseNumber("b", 0)
+ if err != nil {
+ return err
+ }
+ after, err := parseNumber("a", 0)
+ if err != nil {
+ return err
+ }
+ maxHits, err := parseNumber("n", 100)
+ if err != nil {
+ return err
+ }
+
+ manifest, err := config.ReadManifest(ManifestPath)
+ if err != nil {
+ return fmt.Errorf("Failed to read manifest: %w", err)
+ }
+
+ if query == "" && fileFilter == "" {
+ return fmt.Errorf("No query or file filter")
+ } else if query == "" {
+ return searchFile(w, manifest, fileFilter, excludeFileFilter, maxHits, ignoreCase)
+ } else {
+ return search(w, manifest, query, fileFilter, excludeFileFilter, maxHits, ignoreCase, before, after)
+ }
+ })
+}
+
+type MatchedEntry struct {
+ Line int
+ Start int
+ End int
+}
+
+func restShowFile(w http.ResponseWriter, manifest *config.Manifest, path string, query string, ignoreCase bool) error {
+ pattern := query
+ if ignoreCase {
+ pattern = "(?i)" + pattern
+ }
+ re, err := stdregexp.Compile(pattern)
+ if err != nil {
+ return err
+ }
+
+ file, err := os.Open(filepath.Join(CodeDir, path))
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ setHeaders(w)
+ if err := writeJsonFileHeader(w, manifest, path, nil); err != nil {
+ return err
+ }
+ if _, err := w.Write([]byte(",\"content\":\"")); err != nil {
+ return err
+ }
+
+ i := 1
+ var matchedEntries []MatchedEntry
+ scanner := bufio.NewScanner(file)
+ // Got this error with a 68kB line: bufio.Scanner: token too long
+ const maxCapacity = 1024 * 1024 // 1 MB
+ buf := make([]byte, maxCapacity)
+ scanner.Buffer(buf, maxCapacity)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if _, err := w.Write([]byte(escapeJsonString(line + "\n"))); err != nil {
+ return err
+ }
+
+ if query != "" {
+ matches := re.FindStringSubmatchIndex(line)
+ if matches != nil {
+ matchedEntries = append(matchedEntries, MatchedEntry{
+ Line: i,
+ Start: matches[0],
+ End: matches[1],
+ })
+ }
+ }
+ i = i + 1
+ }
+
+ if _, err = w.Write([]byte("\",\"matches\":[")); err != nil {
+ return err
+ }
+ for i, entry := range matchedEntries {
+ if err := maybeWriteComma(w, i > 0); err != nil {
+ return err
+ }
+ entryStr := fmt.Sprintf("{\"line\":%d,\"range\":[%d,%d]}", entry.Line, entry.Start, entry.End)
+ if _, err := w.Write([]byte(entryStr)); err != nil {
+ return err
+ }
+ }
+
+ if _, err = w.Write([]byte(fmt.Sprintf("],\"updatedAt\":%d}", manifest.UpdatedAt.UnixMilli()))); err != nil {
+ return err
+ }
+
+ return scanner.Err()
+}
+
+func RestFileHandler(w http.ResponseWriter, request *http.Request) {
+ handleError(w, func() error {
+ if err := request.ParseForm(); err != nil {
+ return err
+ }
+
+ path := request.Form.Get("p")
+ query := request.Form.Get("q")
+ ignoreCase := request.Form.Get("i") != ""
+
+ if strings.Contains(path, "..") {
+ return fmt.Errorf("Path cannot contain \"..\"")
+ }
+
+ manifest, err := config.ReadManifest(ManifestPath)
+ if err != nil {
+ return fmt.Errorf("Failed to read manifest: %w", err)
+ }
+
+ return restShowFile(w, manifest, path, query, ignoreCase)
+ })
+}
+
+func RestListHandler(w http.ResponseWriter, r *http.Request) {
+ handleError(w, func() error {
+ if err := r.ParseForm(); err != nil {
+ return err
+ }
+ path := r.Form.Get("p")
+ if strings.Contains(path, "..") {
+ return fmt.Errorf("Path cannot contain \"..\"")
+ }
+ dirPath := filepath.Join(CodeDir, path)
+ entries, err := os.ReadDir(dirPath)
+ if err != nil {
+ return fmt.Errorf("Failed to read directory: %w", err)
+ }
+
+ files := make([]string, 0)
+ directories := make([]string, 0)
+ for _, entry := range entries {
+ if entry.IsDir() {
+ if entry.Name() != ".git" {
+ directories = append(directories, entry.Name())
+ }
+ } else {
+ files = append(files, entry.Name())
+ }
+ }
+
+ manifest, err := config.ReadManifest(ManifestPath)
+ if err != nil {
+ return fmt.Errorf("Failed to read manifest: %w", err)
+ }
+
+ setHeaders(w)
+ if err := writeJsonFileHeader(w, manifest, path, nil); err != nil {
+ if _, err := w.Write([]byte(fmt.Sprintf("{\"directory\":\"%s\",", escapeJsonString(strings.Trim(path, "/"))))); err != nil {
+ return err
+ }
+ } else if _, err := w.Write([]byte(",")); err != nil {
+ return err
+ }
+
+ filesJson, err := json.Marshal(files)
+ if err != nil {
+ return fmt.Errorf("Failed to marshal files: %w", err)
+ }
+ dirsJson, err := json.Marshal(directories)
+ if err != nil {
+ return fmt.Errorf("Failed to marshal directories: %w", err)
+ }
+
+ if _, err := w.Write([]byte(fmt.Sprintf("\"files\":%s,\"directories\":%s,\"updatedAt\":%d}", filesJson, dirsJson, manifest.UpdatedAt.UnixMilli()))); err != nil {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/cmd/csupdater/github.go b/cmd/csupdater/github.go
new file mode 100644
index 0000000..82bc115
--- /dev/null
+++ b/cmd/csupdater/github.go
@@ -0,0 +1,302 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/freva/codesearch/internal/config"
+)
+
+// --- Structs for parsing GraphQL JSON responses ---
+type graphQLRequest struct {
+ Query string `json:"query"`
+ Variables map[string]interface{} `json:"variables,omitempty"`
+}
+type graphQLResponse struct {
+ Data json.RawMessage `json:"data,omitempty"`
+ Errors []struct {
+ Message string `json:"message"`
+ } `json:"errors,omitempty"`
+}
+
+type responseBatch = map[string]repositoryNode
+type responseOwner struct {
+ RepositoryOwner struct {
+ Repositories repositoriesConnection `json:"repositories"`
+ } `json:"repositoryOwner"`
+}
+type repositoriesConnection struct {
+ Nodes []repositoryNode `json:"nodes"`
+ PageInfo struct {
+ HasNextPage bool `json:"hasNextPage"`
+ EndCursor string `json:"endCursor"`
+ } `json:"pageInfo"`
+}
+
+// repositoryNode is a shared struct for a repo from either type of query.
+type repositoryNode struct {
+ Name string `json:"name"`
+ DefaultBranchRef *struct {
+ Name string `json:"name"`
+ Target struct {
+ OID string `json:"oid"`
+ } `json:"target"`
+ } `json:"defaultBranchRef"`
+ RequestedRef *struct {
+ OID string `json:"oid"`
+ } `json:"requestedRef"`
+}
+
+const ownerRepositoriesQuery = `
+query GetRepositories($owner: String!, $cursor: String) {
+ repositoryOwner(login: $owner) {
+ repositories(first: 100, after: $cursor, ownerAffiliations: OWNER, isFork: false) {
+ nodes {
+ name
+ defaultBranchRef { name, target { oid } }
+ }
+ pageInfo { hasNextPage, endCursor }
+ }
+ }
+}`
+
+var commitShaRegex = regexp.MustCompile(`^[0-9a-f]{40}$`)
+
+// GetAllRepositories resolves all repositories from the configuration.
+func GetAllRepositories(cfg *config.Config, verbose bool) ([]config.Repository, error) {
+ // Use a map to handle duplicates and easily update entries. Key is "server/owner/repo"
+ repoMap := make(map[string]config.Repository)
+ client := &http.Client{Timeout: 30 * time.Second}
+
+ for _, server := range cfg.Servers {
+ var ownersToFetch []string
+ var specificsToFetch []config.Include
+
+ for _, include := range server.Include {
+ if include.Name == "" { // This is an owner-only include
+ ownersToFetch = append(ownersToFetch, include.Owner)
+ } else { // This is a specific repo include
+ specificsToFetch = append(specificsToFetch, include)
+ }
+ }
+
+ // Fetch all repos for the specified owners
+ for _, owner := range ownersToFetch {
+ if verbose {
+ fmt.Printf("Fetching repositories for server '%s' and owner '%s'\n", server.Name, owner)
+ }
+ err := fetchReposForOwner(client, server, owner, repoMap)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch repos for '%s': %w", owner, err)
+ }
+ }
+
+ // Fetch all specific repos in a single batch request
+ if len(specificsToFetch) > 0 {
+ if verbose {
+ fmt.Printf("Fetching repositories for server '%s': %s\n", server.Name, specificsToFetch)
+ }
+ err := fetchSpecificRepos(client, server, specificsToFetch, repoMap)
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch specific repos: %w", err)
+ }
+ }
+ }
+
+ keys := make([]string, 0, len(repoMap))
+ for k := range repoMap {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ result := make([]config.Repository, 0, len(repoMap))
+ for _, key := range keys {
+ result = append(result, repoMap[key])
+ }
+ return result, nil
+}
+
+// fetchReposForOwner handles the paginated GraphQL query for a single owner.
+func fetchReposForOwner(
+ client *http.Client,
+ server *config.Server,
+ owner string,
+ repoMap map[string]config.Repository,
+) error {
+ var cursor *string
+ exclude := regexp.MustCompile(server.Exclude)
+ for {
+ reqBody, _ := json.Marshal(graphQLRequest{
+ Query: ownerRepositoriesQuery,
+ Variables: map[string]interface{}{"owner": owner, "cursor": cursor},
+ })
+
+ gqlResp, err := executeGraphQLQuery[responseOwner](client, server, reqBody)
+ if err != nil {
+ return fmt.Errorf("query failed: %w", err)
+ }
+
+ // Process the fetched nodes
+ for _, node := range gqlResp.RepositoryOwner.Repositories.Nodes {
+ if node.DefaultBranchRef == nil {
+ continue
+ }
+ fullName := fmt.Sprintf("%s/%s", owner, node.Name)
+ if server.Exclude != "" && exclude.MatchString(fullName) {
+ continue
+ }
+
+ repoMap[fmt.Sprintf("%s/%s", server.Name, fullName)] = config.Repository{
+ Server: server.Name,
+ Owner: owner,
+ Name: node.Name,
+ Branch: node.DefaultBranchRef.Name,
+ Commit: node.DefaultBranchRef.Target.OID,
+ }
+ }
+
+ if !gqlResp.RepositoryOwner.Repositories.PageInfo.HasNextPage {
+ break
+ }
+ endCursor := gqlResp.RepositoryOwner.Repositories.PageInfo.EndCursor
+ cursor = &endCursor
+ }
+ return nil
+}
+
+// fetchSpecificRepos builds and executes a single GraphQL query for multiple specific repos.
+func fetchSpecificRepos(
+ client *http.Client,
+ server *config.Server,
+ requests []config.Include,
+ repoMap map[string]config.Repository,
+) error {
+ var b strings.Builder
+ exclude := regexp.MustCompile(server.Exclude)
+ b.WriteString("query {")
+
+ // Build the dynamic query with aliases
+ for i, r := range requests {
+ // Check for exclusion before adding to the query
+ fullName := fmt.Sprintf("%s/%s", r.Owner, r.Name)
+ if server.Exclude != "" && exclude.MatchString(fullName) {
+ continue
+ }
+
+ // If ref is a commit SHA, we can directly add it to the map
+ if r.Ref != "" && commitShaRegex.MatchString(r.Ref) {
+ repoMap[fmt.Sprintf("%s/%s", server.Name, fullName)] = config.Repository{
+ Server: server.Name,
+ Owner: r.Owner,
+ Name: r.Name,
+ Branch: r.Ref,
+ Commit: r.Ref,
+ }
+ continue
+ }
+
+ // If no ref is specified, get the default branch and HEAD commit, otherwise filter on the requested ref (branch)
+ refPart := "defaultBranchRef { name, target { oid } }"
+ if r.Ref != "" {
+ refPart = fmt.Sprintf(`requestedRef: object(expression: "refs/heads/%s") { oid }`, r.Ref)
+ }
+
+ b.WriteString(fmt.Sprintf(`
+ repo_%d: repository(owner: %q, name: %q) {
+ %s
+ }
+ `, i, r.Owner, r.Name, refPart))
+ }
+ b.WriteString("}")
+
+ if b.String() == "query {}" { // All specifics were excluded
+ return nil
+ }
+
+ reqBody, _ := json.Marshal(graphQLRequest{Query: b.String()})
+ gqlResp, err := executeGraphQLQuery[responseBatch](client, server, reqBody)
+ if err != nil {
+ return err
+ }
+
+ for i, node := range *gqlResp {
+ // The key from the response map is the alias (e.g., "repo_0")
+ // We need the original request to get the user-specified ref.
+ var originalIndex int
+ _, err := fmt.Sscanf(i, "repo_%d", &originalIndex)
+ if err != nil {
+ return fmt.Errorf("failed to parse response key '%s': %w", i, err)
+ }
+ originalReq := requests[originalIndex]
+
+ repo := config.Repository{Server: server.Name, Owner: originalReq.Owner, Name: originalReq.Name}
+
+ if originalReq.Ref != "" && node.RequestedRef != nil && node.RequestedRef.OID != "" {
+ repo.Branch = originalReq.Ref
+ repo.Commit = node.RequestedRef.OID
+ } else if node.DefaultBranchRef != nil {
+ repo.Branch = node.DefaultBranchRef.Name
+ repo.Commit = node.DefaultBranchRef.Target.OID
+ }
+
+ repoMap[fmt.Sprintf("%s/%s/%s", server.Name, originalReq.Owner, originalReq.Name)] = repo
+ }
+
+ return nil
+}
+
+func executeGraphQLQuery[T any](client *http.Client, server *config.Server, body []byte) (*T, error) {
+ apiURL, err := url.Parse(server.ApiURL)
+ if err != nil {
+ return nil, fmt.Errorf("invalid API URL '%s': %w", server.ApiURL, err)
+ }
+ apiURL.Path += "/graphql"
+
+ req, err := http.NewRequest("POST", apiURL.String(), bytes.NewBuffer(body))
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+ if server.Token != "" {
+ req.Header.Set("Authorization", "bearer "+server.Token)
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ respBody, _ := io.ReadAll(resp.Body)
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("graphql query failed with status %d: %s", resp.StatusCode, string(respBody))
+ }
+
+ var gqlResp graphQLResponse
+ if err := json.Unmarshal(respBody, &gqlResp); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal graphql response: %w", err)
+ }
+
+ if len(gqlResp.Errors) > 0 {
+ var errorMessages []string
+ for _, e := range gqlResp.Errors {
+ errorMessages = append(errorMessages, e.Message)
+ }
+ return nil, fmt.Errorf("api returned errors: %s", strings.Join(errorMessages, ", "))
+ }
+
+ var gqlData T
+ if err := json.Unmarshal(gqlResp.Data, &gqlData); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal 'data' field: %w", err)
+ }
+
+ return &gqlData, nil
+}
diff --git a/cmd/csupdater/github_test.go b/cmd/csupdater/github_test.go
new file mode 100644
index 0000000..a664aeb
--- /dev/null
+++ b/cmd/csupdater/github_test.go
@@ -0,0 +1,283 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/freva/codesearch/internal/config"
+)
+
+func createMockServer(handler http.HandlerFunc) (*http.Client, *httptest.Server) {
+ s := httptest.NewServer(handler)
+ c := s.Client()
+ return c, s
+}
+
+func TestExecuteGraphQLQuery(t *testing.T) {
+ t.Run("non-200 status code", func(t *testing.T) {
+ client, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte("Internal Server Error"))
+ })
+ defer server.Close()
+
+ serverConfig := &config.Server{Name: "test-server", ApiURL: server.URL}
+ _, err := executeGraphQLQuery[*struct{}](client, serverConfig, []byte(`{}`))
+ if err == nil {
+ t.Fatal("Expected error for non-200 status, got nil")
+ }
+ if !strings.Contains(err.Error(), "graphql query failed with status 500") {
+ t.Errorf("Expected 'graphql query failed' error, got %v", err)
+ }
+ })
+
+ t.Run("invalid json response", func(t *testing.T) {
+ client, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`not a valid json`))
+ })
+ defer server.Close()
+
+ serverConfig := &config.Server{Name: "test-server", ApiURL: server.URL}
+ _, err := executeGraphQLQuery[*struct{}](client, serverConfig, []byte(`{}`))
+ if err == nil {
+ t.Fatal("Expected error for invalid JSON response, got nil")
+ }
+ if !strings.Contains(err.Error(), "failed to unmarshal graphql response") {
+ t.Errorf("Expected 'failed to unmarshal graphql response' error, got %v", err)
+ }
+ })
+
+ t.Run("graphql errors", func(t *testing.T) {
+ client, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"data":null,"errors":[{"message":"Error 1"},{"message":"Error 2"}]}`))
+ })
+ defer server.Close()
+
+ serverConfig := &config.Server{Name: "test-server", ApiURL: server.URL}
+ _, err := executeGraphQLQuery[struct{}](client, serverConfig, []byte(`{}`))
+ if err == nil {
+ t.Fatal("Expected error for GraphQL errors, got nil")
+ }
+ if !strings.Contains(err.Error(), "api returned errors: Error 1, Error 2") {
+ t.Errorf("Expected 'api returned errors' error, got %v", err)
+ }
+ })
+}
+
+func TestFetchReposForOwner(t *testing.T) {
+ page := 0
+ client, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody graphQLRequest
+ err := json.NewDecoder(r.Body).Decode(&reqBody)
+ if err != nil {
+ http.Error(w, "Bad Request", http.StatusBadRequest)
+ return
+ }
+
+ var resp []byte
+ if page == 0 {
+ resp = []byte(`{"data":{"repositoryOwner":{"repositories":{"nodes":[
+{"name":"repo1","defaultBranchRef":{"name":"main","target":{"oid":"sha1"}}},
+{"name":"excluded-repo","defaultBranchRef":{"name":"main","target":{"oid":"sha-excluded"}}}],
+"pageInfo":{"hasNextPage":true,"endCursor":"cursor1"}}}}}`)
+ page++
+ } else if page == 1 {
+ resp = []byte(`{"data":{"repositoryOwner":{"repositories":{"nodes":[
+{"name":"repo2","defaultBranchRef":{"name":"dev","target":{"oid":"sha2"}}}],
+"pageInfo":{"hasNextPage":false}}}}}`)
+ page++
+ } else {
+ t.Errorf("Unexpected call to mock server on page %d", page)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(resp)
+ })
+ defer server.Close()
+
+ serverConfig := &config.Server{
+ Name: "test-server",
+ ApiURL: server.URL,
+ Exclude: "excluded-repo",
+ }
+ repoMap := make(map[string]config.Repository)
+
+ err := fetchReposForOwner(client, serverConfig, "test-owner", repoMap)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ expectedRepos := map[string]config.Repository{
+ "test-server/test-owner/repo1": {
+ Server: "test-server", Owner: "test-owner", Name: "repo1", Branch: "main", Commit: "sha1",
+ },
+ "test-server/test-owner/repo2": {
+ Server: "test-server", Owner: "test-owner", Name: "repo2", Branch: "dev", Commit: "sha2",
+ },
+ }
+
+ if !reflect.DeepEqual(repoMap, expectedRepos) {
+ t.Errorf("Expected repos to match:\nExpected: %+v\nGot: %+v", expectedRepos, repoMap)
+ }
+}
+
+func TestFetchSpecificRepos(t *testing.T) {
+ t.Run("specific repos with branches and SHAs", func(t *testing.T) {
+ client, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody graphQLRequest
+ _ = json.NewDecoder(r.Body).Decode(&reqBody)
+ if regexp.MustCompile(`\s+`).ReplaceAllString(reqBody.Query, " ") != "query { "+
+ "repo_0: repository(owner: \"test-owner\", name: \"repoA\") { requestedRef: object(expression: \"refs/heads/feature\") { oid } } "+
+ "repo_1: repository(owner: \"test-owner\", name: \"repoB\") { defaultBranchRef { name, target { oid } } } }" {
+ t.Errorf("Unexpected query: %s", reqBody.Query)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"data":{
+"repo_0":{"requestedRef":{"oid":"sha-feature"}},
+"repo_1":{"defaultBranchRef":{"name":"main","target":{"oid":"sha-main"}}}}}`))
+ })
+ defer server.Close()
+
+ serverConfig := &config.Server{
+ Name: "test-server",
+ ApiURL: server.URL,
+ }
+ requests := []config.Include{
+ {Owner: "test-owner", Name: "repoA", Ref: "feature"},
+ {Owner: "test-owner", Name: "repoB", Ref: ""}, // No ref, should get default
+ {Owner: "test-owner", Name: "repoC", Ref: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"}, // Direct SHA
+ {Owner: "test-owner", Name: "excluded-specific", Ref: ""},
+ }
+ serverConfig.Exclude = "excluded-specific"
+
+ repoMap := make(map[string]config.Repository)
+ err := fetchSpecificRepos(client, serverConfig, requests, repoMap)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ expectedRepos := map[string]config.Repository{
+ "test-server/test-owner/repoA": {
+ Server: "test-server", Owner: "test-owner", Name: "repoA", Branch: "feature", Commit: "sha-feature",
+ },
+ "test-server/test-owner/repoB": {
+ Server: "test-server", Owner: "test-owner", Name: "repoB", Branch: "main", Commit: "sha-main",
+ },
+ "test-server/test-owner/repoC": {
+ Server: "test-server", Owner: "test-owner", Name: "repoC", Branch: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", Commit: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
+ },
+ }
+
+ if !reflect.DeepEqual(repoMap, expectedRepos) {
+ t.Errorf("Expected repos to match:\nExpected: %+v\nGot: %+v", expectedRepos, repoMap)
+ }
+ })
+
+ t.Run("all specific repos excluded", func(t *testing.T) {
+ client, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ t.Fatal("Mock server should not be called if all repos are excluded")
+ })
+ defer server.Close()
+
+ serverConfig := &config.Server{
+ Name: "test-server",
+ ApiURL: server.URL,
+ Exclude: ".*", // Exclude all
+ }
+ requests := []config.Include{
+ {Owner: "test-owner", Name: "repoA", Ref: "feature"},
+ }
+
+ repoMap := make(map[string]config.Repository)
+ err := fetchSpecificRepos(client, serverConfig, requests, repoMap)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ if len(repoMap) != 0 {
+ t.Errorf("Expected 0 repos, got %d", len(repoMap))
+ }
+ })
+}
+
+func TestGetAllRepositories(t *testing.T) {
+ ownerPage := 0
+ _, server := createMockServer(func(w http.ResponseWriter, r *http.Request) {
+ var reqBody graphQLRequest
+ _ = json.NewDecoder(r.Body).Decode(&reqBody)
+
+ var query string
+ var variables string
+ if strings.Contains(reqBody.Query, "GetRepositories") {
+ var resp []byte
+ query = ownerRepositoriesQuery
+ if ownerPage == 0 {
+ variables = `{"cursor":null,"owner":"test-owner-all"}`
+ resp = []byte(`{"data":{"repositoryOwner":{"repositories":{"nodes":[
+{"name":"owner-repo1","defaultBranchRef":{"name":"main","target":{"oid":"owner-sha1"}}}],
+"pageInfo":{"hasNextPage":true,"endCursor":"owner-cursor1"}}}}}`)
+ ownerPage++
+ } else {
+ variables = `{"cursor":"owner-cursor1","owner":"test-owner-all"}`
+ resp = []byte(`{"data":{"repositoryOwner":{"repositories":{"nodes":[
+{"name":"owner-repo2","defaultBranchRef":{"name":"dev","target":{"oid":"owner-sha2"}}}],
+"pageInfo":{"hasNextPage":false}}}}}`)
+ }
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(resp)
+ } else {
+ query = `query { repo_0: repository(owner: "test-owner", name: "specific-repo1") { defaultBranchRef { name, target { oid } } } }`
+ variables = `null`
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"data":{"repo_0":{"defaultBranchRef":{"name":"master","target":{"oid":"specific-sha1"}}}}}`))
+ }
+
+ re := regexp.MustCompile(`\s+`)
+ if re.ReplaceAllString(reqBody.Query, " ") != re.ReplaceAllString(query, " ") {
+ t.Errorf("Unexpected query: %s", reqBody.Query)
+ }
+ if v, _ := json.Marshal(reqBody.Variables); string(v) != variables {
+ t.Errorf("Unexpected variables: %s", string(v))
+ }
+ })
+ defer server.Close()
+
+ cfg := &config.Config{
+ Servers: map[string]*config.Server{
+ "github": {
+ Name: "github.com",
+ ApiURL: server.URL,
+ Include: []config.Include{
+ {Owner: "test-owner-all"}, // Fetch all for this owner
+ {Owner: "test-owner", Name: "specific-repo1", Ref: ""}, // Fetch specific repo
+ {Owner: "test-owner", Name: "specific-sha-repo", Ref: "c1d2e3f4c5d6e7f8c9d0e1f2a3b4c5d6e7f8c9d0"}, // Specific SHA
+ },
+ Exclude: "not-to-be-included",
+ },
+ },
+ }
+
+ repos, err := GetAllRepositories(cfg, false)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ expectedRepos := []config.Repository{
+ {Server: "github.com", Owner: "test-owner-all", Name: "owner-repo1", Branch: "main", Commit: "owner-sha1"},
+ {Server: "github.com", Owner: "test-owner-all", Name: "owner-repo2", Branch: "dev", Commit: "owner-sha2"},
+ {Server: "github.com", Owner: "test-owner", Name: "specific-repo1", Branch: "master", Commit: "specific-sha1"},
+ {Server: "github.com", Owner: "test-owner", Name: "specific-sha-repo", Branch: "c1d2e3f4c5d6e7f8c9d0e1f2a3b4c5d6e7f8c9d0", Commit: "c1d2e3f4c5d6e7f8c9d0e1f2a3b4c5d6e7f8c9d0"},
+ }
+
+ if !reflect.DeepEqual(repos, expectedRepos) {
+ t.Errorf("Expected repos to match:\nExpected: %+v\nGot: %+v", expectedRepos, repos)
+ }
+}
diff --git a/cmd/csupdater/indices.go b/cmd/csupdater/indices.go
new file mode 100644
index 0000000..7169da3
--- /dev/null
+++ b/cmd/csupdater/indices.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+ "fmt"
+ "io/fs"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/freva/codesearch/index"
+ "github.com/freva/codesearch/internal/config"
+)
+
+// UpdateIndices update file indexes and the main code search index.
+func UpdateIndices(config *config.Config, verbose bool) error {
+ start := time.Now()
+ const maxLines = 128
+ var lineCounter int
+ var currentFile *os.File
+ var err error
+
+ fileListsPath := filepath.Join(config.FileListsDir)
+ if err := os.RemoveAll(fileListsPath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to remove old file lists directory '%s': %w", fileListsPath, err)
+ }
+ if err := os.MkdirAll(fileListsPath, 0755); err != nil {
+ return fmt.Errorf("failed to create file lists directory '%s': %w", fileListsPath, err)
+ }
+
+ codeIndex := index.Create(config.CodeIndexPath + "~")
+ codeIndex.LogSkip = verbose
+ codeIndex.AddRoots([]index.Path{index.MakePath(config.CodeDir)})
+ fileIndex := index.Create(config.FileIndexPath + "~")
+ fileIndex.AddRoots([]index.Path{index.MakePath(fileListsPath)})
+ fileIndex.LogSkip = verbose
+
+ writeToIndex := func() error {
+ if currentFile == nil {
+ return nil
+ }
+ name := currentFile.Name()
+ currentFile.Close()
+ return fileIndex.AddFile(name)
+ }
+
+ for path := range walkFiles(config.CodeDir) {
+ if err := codeIndex.AddFile(path); err != nil {
+ return fmt.Errorf("failed to add file %s to index: %w", path, err)
+ }
+
+ if lineCounter%maxLines == 0 {
+ if err := writeToIndex(); err != nil {
+ return fmt.Errorf("failed to add file to file index: %w", err)
+ }
+ fileIndexPath := filepath.Join(fileListsPath, fmt.Sprintf("%0*x", 5, lineCounter/maxLines))
+ currentFile, err = os.Create(fileIndexPath)
+ if err != nil {
+ return fmt.Errorf("failed to create file index file: %w", err)
+ }
+ }
+ lineCounter++
+
+ _, err = fmt.Fprintln(currentFile, strings.TrimPrefix(path, config.CodeDir+"/"))
+ if err != nil {
+ return err
+ }
+ }
+
+ if err := writeToIndex(); err != nil {
+ return fmt.Errorf("failed to add file to file index: %w", err)
+ }
+
+ codeIndex.Flush()
+ fileIndex.Flush()
+
+ if err := os.Rename(config.CodeIndexPath+"~", config.CodeIndexPath); err != nil {
+ return fmt.Errorf("failed to rename code index file: %w", err)
+ }
+ if err := os.Rename(config.FileIndexPath+"~", config.FileIndexPath); err != nil {
+ return fmt.Errorf("failed to rename file index file: %w", err)
+ }
+
+ log.Printf("Indexed %d paths in %s.\n", lineCounter, time.Since(start).Round(10*time.Millisecond))
+ return nil
+}
+
+func walkFiles(root string) <-chan string {
+ paths := make(chan string)
+
+ go func() {
+ defer close(paths)
+
+ err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if _, elem := filepath.Split(path); elem != "" {
+ if elem == ".git" || elem[0] == '#' || elem[0] == '~' || elem[len(elem)-1] == '~' {
+ if info.IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+ }
+ if info != nil && info.Mode()&os.ModeType == 0 {
+ paths <- path
+ }
+ return nil
+ })
+
+ if err != nil {
+ fmt.Printf("error walking the path %q: %v\n", root, err)
+ }
+ }()
+
+ return paths
+}
diff --git a/cmd/csupdater/indices_test.go b/cmd/csupdater/indices_test.go
new file mode 100644
index 0000000..8ee780a
--- /dev/null
+++ b/cmd/csupdater/indices_test.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestWalkFiles(t *testing.T) {
+ tmp := t.TempDir()
+ os.MkdirAll(filepath.Join(tmp, "some/nested/dir"), 0755)
+ os.WriteFile(filepath.Join(tmp, "some/nested/dir/file.json"), []byte(""), 0644)
+ os.WriteFile(filepath.Join(tmp, "some/nested/dir/file.json~"), []byte(""), 0644)
+ os.WriteFile(filepath.Join(tmp, "some/nested/.gitignore"), []byte(""), 0644)
+ os.WriteFile(filepath.Join(tmp, "a.txt"), []byte(""), 0644)
+ os.WriteFile(filepath.Join(tmp, "#skip.txt"), []byte(""), 0644)
+ os.Mkdir(filepath.Join(tmp, ".git"), 0755)
+ os.WriteFile(filepath.Join(tmp, ".git", "x"), []byte(""), 0644)
+
+ var got []string
+ for f := range walkFiles(tmp) {
+ got = append(got, strings.TrimPrefix(f, tmp+"/"))
+ }
+ expected := []string{"a.txt", "some/nested/.gitignore", "some/nested/dir/file.json"}
+ if !reflect.DeepEqual(got, expected) {
+ t.Errorf("walkFiles returned %v, expected %v", got, expected)
+ }
+}
diff --git a/cmd/csupdater/sync.go b/cmd/csupdater/sync.go
new file mode 100644
index 0000000..692948e
--- /dev/null
+++ b/cmd/csupdater/sync.go
@@ -0,0 +1,235 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "net/url"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/freva/codesearch/internal/config"
+)
+
+type IShellCommand interface {
+ CombinedOutput() ([]byte, error)
+ Run(stdout io.Writer, stderr io.Writer) error
+}
+type execShellCommand struct {
+ cmd *exec.Cmd
+}
+
+func (exc execShellCommand) CombinedOutput() ([]byte, error) {
+ return exc.cmd.CombinedOutput()
+}
+func (exc execShellCommand) Run(stdout io.Writer, stderr io.Writer) error {
+ exc.cmd.Stdout = stdout
+ exc.cmd.Stderr = stderr
+ return exc.cmd.Run()
+}
+
+var command = func(name string, arg ...string) IShellCommand {
+ return execShellCommand{cmd: exec.Command(name, arg...)}
+}
+
+// SyncRepos clones new repos, updates existing ones, and removes any that are no longer needed.
+func SyncRepos(cfg *config.Config, verbose bool) error {
+ start := time.Now()
+ var cloned, updated, noop int
+ manifest, err := config.ReadManifest(cfg.ManifestPath)
+ if err != nil {
+ return err
+ }
+
+ orphans, err := listWithMaxDepth(cfg.CodeDir, 3)
+ if err != nil {
+ return fmt.Errorf("could not scan for orphaned directories: %w", err)
+ }
+
+ for _, repo := range manifest.Repositories {
+ delete(orphans, repo.RepoDir())
+
+ localPath := filepath.Join(cfg.CodeDir, repo.RepoDir())
+ if _, err := os.Stat(localPath); err == nil {
+ if info, err := os.Stat(filepath.Join(localPath, ".git", "index")); err != nil || info.Size() == 0 {
+ log.Printf("WARNING: Corrupt .git/index found in %s. Removing directory.", localPath)
+ err = os.RemoveAll(localPath)
+ if err != nil {
+ return fmt.Errorf("failed to remove corrupt repository: %w", err)
+ }
+ } else {
+ hasUpdated, err := updateRepo(repo, localPath, verbose)
+ if err != nil {
+ return fmt.Errorf("ERROR: Failed to update %s: %w", repo.RepoDir(), err)
+ }
+ if hasUpdated {
+ updated++
+ } else {
+ noop++
+ }
+ continue
+ }
+ }
+ cloned++
+ err := cloneRepo(cfg, repo, localPath, verbose)
+ if err != nil {
+ return fmt.Errorf("ERROR: Failed to clone %s: %w", repo.RepoDir(), err)
+ }
+
+ }
+
+ cleanupOrphans(orphans, cfg.CodeDir)
+ log.Printf("Synced %d repositories: %d new, %d updated, %d unchanged in %s.\n",
+ cloned+updated+noop, cloned, updated, noop, time.Since(start).Round(10*time.Millisecond))
+ return nil
+}
+
+// cloneRepo handles cloning a new repository.
+func cloneRepo(config *config.Config, repo *config.Repository, localPath string, verbose bool) error {
+ serverConfig, ok := config.Servers[repo.Server]
+ if !ok {
+ return fmt.Errorf("no server config found for '%s'", repo.Server)
+ }
+
+ cloneURL, err := buildCloneURL(serverConfig.CloneURL, repo.Owner, repo.Name)
+ if err != nil {
+ return fmt.Errorf("could not build clone URL: %w", err)
+ }
+
+ if verbose {
+ log.Printf("%s: Cloning", repo.RepoDir())
+ }
+ if err := os.MkdirAll(filepath.Dir(localPath), 0755); err != nil {
+ return err
+ }
+
+ err = runGitCommand(verbose, "clone", cloneURL, localPath)
+ if err != nil {
+ return err
+ }
+
+ return runGitCommand(verbose, "-C", localPath, "checkout", repo.Commit)
+}
+
+// updateRepo handles updating an existing local repository. Returns true if the repo was updated, false if it was already up-to-date.
+func updateRepo(repo *config.Repository, localPath string, verbose bool) (bool, error) {
+ output, err := command("git", "-C", localPath, "rev-parse", "HEAD").CombinedOutput()
+ if err != nil {
+ log.Printf("could not determine current commit: %v", err)
+ } else if strings.TrimSpace(string(output)) == repo.Commit {
+ if verbose {
+ log.Printf("%s: Already up-to-date", repo.RepoDir())
+ }
+ return false, nil
+ }
+
+ if verbose {
+ log.Printf("%s: Updating", repo.RepoDir())
+ }
+ if err := runGitCommand(verbose, "-C", localPath, "fetch"); err != nil {
+ return false, err
+ }
+ return true, runGitCommand(verbose, "-C", localPath, "checkout", repo.Commit)
+}
+
+func runGitCommand(verbose bool, args ...string) error {
+ var outputBuf bytes.Buffer
+ cmd := command("git", args...)
+ var err error
+ if verbose {
+ err = cmd.Run(os.Stdout, os.Stderr)
+ } else {
+ err = cmd.Run(&outputBuf, &outputBuf)
+ }
+ if err != nil {
+ return fmt.Errorf("git %+q failed: %w\nOutput: %s\n", args, err, outputBuf.String())
+ }
+ return nil
+}
+
+// buildCloneURL constructs a valid git clone URL based on the logic from the original shell script.
+func buildCloneURL(baseURL, owner, repoName string) (string, error) {
+ repoPath := fmt.Sprintf("%s/%s.git", owner, repoName)
+
+ if strings.HasPrefix(baseURL, "https://") || strings.HasPrefix(baseURL, "ssh://") {
+ u, err := url.Parse(baseURL)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse URL '%s': %w", baseURL, err)
+ }
+ u.Path = path.Join(u.Path, repoPath)
+ return u.String(), nil
+ }
+
+ // Handle SCP-like syntax, e.g., "git@github.com"
+ scpPattern := regexp.MustCompile(`^(?:[a-zA-Z0-9_.-]+@)?[a-z][a-z0-9-]+\.[a-z][a-z0-9.-]+$`)
+ if scpPattern.MatchString(baseURL) {
+ // For SCP syntax, the separator between host and path is a colon.
+ return fmt.Sprintf("%s:%s", baseURL, repoPath), nil
+ }
+
+ return "", fmt.Errorf("unsupported or malformed URL format: '%s'", baseURL)
+}
+
+// cleanupOrphans removes all directories remaining in the orphan map.
+func cleanupOrphans(orphans map[string]bool, codeDir string) {
+ if len(orphans) == 0 {
+ return
+ }
+
+ dirsToRemove := make([]string, 0, len(orphans))
+ for dir := range orphans {
+ dirsToRemove = append(dirsToRemove, dir)
+ }
+
+ // Sort keys to ensure child directories are removed before parents
+ sort.Sort(sort.Reverse(sort.StringSlice(dirsToRemove)))
+
+ for _, dir := range dirsToRemove {
+ log.Printf("Removing orphaned path: %s", dir)
+ fullPath := filepath.Join(codeDir, dir)
+ if err := os.RemoveAll(fullPath); err != nil {
+ log.Printf("ERROR: Failed to remove %s: %v", fullPath, err)
+ }
+ }
+}
+
+// listWithMaxDepth returns all paths under given root path, relative to it, within a given max depth.
+func listWithMaxDepth(root string, maxDepth int) (map[string]bool, error) {
+ paths := make(map[string]bool)
+ cleanedRoot := filepath.Clean(root)
+
+ err := filepath.WalkDir(cleanedRoot, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ relPath, err := filepath.Rel(cleanedRoot, path)
+ if err != nil {
+ return err
+ }
+
+ if relPath == "." {
+ return nil
+ }
+
+ delete(paths, filepath.Dir(relPath))
+ paths[relPath] = true
+ if d.IsDir() && strings.Count(relPath, string(os.PathSeparator)) == maxDepth-1 {
+ return filepath.SkipDir
+ }
+ return nil
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ return paths, nil
+}
diff --git a/cmd/csupdater/sync_test.go b/cmd/csupdater/sync_test.go
new file mode 100644
index 0000000..b22bc24
--- /dev/null
+++ b/cmd/csupdater/sync_test.go
@@ -0,0 +1,373 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/freva/codesearch/internal/config"
+)
+
+type mockCmd struct {
+ expectedCmdArgs []string
+ stdout string
+ stderr string
+ err error
+}
+
+func (mc *mockCmd) Run(stdout io.Writer, stderr io.Writer) error {
+ if _, err := stdout.Write([]byte(mc.stdout)); err != nil {
+ return err
+ }
+ if _, err := stderr.Write([]byte(mc.stderr)); err != nil {
+ return err
+ }
+ return mc.err
+}
+func (mc *mockCmd) CombinedOutput() ([]byte, error) {
+ if mc.err != nil {
+ return nil, mc.err
+ }
+ return []byte(mc.stdout + mc.stderr), nil
+}
+
+func setMockGitCommand(t *testing.T, entries []mockCmd) func() {
+ originalCommand := command
+
+ command = func(name string, args ...string) IShellCommand {
+ if len(entries) == 0 {
+ t.Fatalf("No more git command mocks expected for %s %v", name, args)
+ }
+
+ nextMock := entries[0]
+ entries = entries[1:]
+
+ actualCmdArgs := append([]string{name}, args...)
+ if !reflect.DeepEqual(actualCmdArgs, nextMock.expectedCmdArgs) {
+ t.Fatalf("Mock mismatch:\nExpected: %v\nActual: %v\nRemaining mocks: %+v",
+ nextMock.expectedCmdArgs, actualCmdArgs, entries)
+ }
+
+ return &nextMock
+ }
+
+ return func() {
+ command = originalCommand
+ if len(entries) > 0 {
+ t.Errorf("Not all mock commands were used. Remaining mocs: %+v", entries)
+ }
+ }
+}
+
+func createDummyManifest(t *testing.T, path string, repos []*config.Repository) {
+ manifest := &config.Manifest{
+ Servers: make(map[string]string),
+ Repositories: make(map[string]*config.Repository),
+ UpdatedAt: time.Now(),
+ }
+ for _, repo := range repos {
+ manifest.Repositories[repo.RepoDir()] = repo
+ if _, ok := manifest.Servers[repo.Server]; !ok {
+ manifest.Servers[repo.Server] = "http://mock-api.com/" + repo.Server
+ }
+ }
+ data, err := json.MarshalIndent(manifest, "", " ")
+ if err != nil {
+ t.Fatalf("Failed to marshal manifest: %v", err)
+ }
+ if err := os.WriteFile(path, data, 0644); err != nil {
+ t.Fatalf("Failed to write manifest file: %v", err)
+ }
+}
+
+func TestBuildCloneURL(t *testing.T) {
+ tests := []struct{ name, baseURL, expected string }{
+ {name: "HTTPS URL", baseURL: "https://github.com", expected: "https://github.com/owner/repo.git"},
+ {name: "SSH URL", baseURL: "ssh://git@github.com", expected: "ssh://git@github.com/owner/repo.git"},
+ {name: "SCP-like URL", baseURL: "git@github.com", expected: "git@github.com:owner/repo.git"},
+ {name: "SCP-like URL with subdomain", baseURL: "git@sub.domain.com", expected: "git@sub.domain.com:owner/repo.git"},
+ {name: "Malformed URL", baseURL: "http://%gh.com", expected: ""},
+ {name: "Unsupported format", baseURL: "ftp://host.com", expected: ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ actual, err := buildCloneURL(tt.baseURL, "owner", "repo")
+ expectErr := tt.expected == ""
+ if (err != nil) != expectErr {
+ t.Errorf("Expected error: %v, Got error: %v", expectErr, err)
+ }
+ if actual != tt.expected {
+ t.Errorf("Expected URL '%s', Got '%s'", tt.expected, actual)
+ }
+ })
+ }
+}
+
+func TestUpdateRepo(t *testing.T) {
+ log.SetOutput(io.Discard)
+ defer log.SetOutput(os.Stderr)
+
+ tempDir := t.TempDir()
+ localPath := filepath.Join(tempDir, "test-repo")
+ os.MkdirAll(filepath.Join(localPath, ".git"), 0755)
+
+ repo := &config.Repository{
+ Server: "github", Owner: "test-owner", Name: "test-repo",
+ Branch: "main", Commit: "newsha1234567890123456789012345678901234567890",
+ }
+
+ t.Run("repo already up-to-date", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "-C", localPath, "rev-parse", "HEAD"}, stdout: repo.Commit + "\n"},
+ })
+
+ updated, err := updateRepo(repo, localPath, false)
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ if updated {
+ t.Error("Expected repo to be not updated, but it was")
+ }
+ })
+
+ t.Run("repo needs update and succeeds", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "-C", localPath, "rev-parse", "HEAD"}, stdout: "oldsha\n"},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "fetch"}, stdout: ""},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "checkout", repo.Commit}, stdout: ""},
+ })
+
+ updated, err := updateRepo(repo, localPath, false)
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+ if !updated {
+ t.Error("Expected repo to be updated, but it was not")
+ }
+ })
+
+ t.Run("rev-parse fails", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "-C", localPath, "rev-parse", "HEAD"}, stderr: "fatal: bad object\n", err: fmt.Errorf("exit status 1")},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "fetch"}, stdout: ""},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "checkout", repo.Commit}, stdout: ""},
+ })
+
+ updated, err := updateRepo(repo, localPath, false)
+ if err != nil {
+ t.Errorf("Expected no error from updateRepo when rev-parse fails but fetch/checkout succeed. Got error: %v", err)
+ }
+ if !updated {
+ t.Error("Expected update process to continue and result in an update attempt despite rev-parse error.")
+ }
+ })
+
+ t.Run("fetch fails", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "-C", localPath, "rev-parse", "HEAD"}, stdout: "oldsha\n"},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "fetch"}, stderr: "fatal: network error\n", err: fmt.Errorf("exit status 1")},
+ })
+
+ _, err := updateRepo(repo, localPath, false)
+ if err == nil {
+ t.Fatal("Expected error from fetch, got nil")
+ }
+ if !strings.Contains(err.Error(), `git ["-C"`) || !strings.Contains(err.Error(), `"fetch"] failed`) || !strings.Contains(err.Error(), "network error") {
+ t.Errorf("Expected fetch error, got %v", err)
+ }
+ })
+
+ t.Run("checkout fails", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "-C", localPath, "rev-parse", "HEAD"}, stdout: "oldsha\n"},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "fetch"}, stdout: ""},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "checkout", repo.Commit}, stderr: "fatal: branch not found\n", err: fmt.Errorf("exit status 1")},
+ })
+
+ _, err := updateRepo(repo, localPath, false)
+ if err == nil {
+ t.Fatal("Expected error from checkout, got nil")
+ }
+ if !strings.Contains(err.Error(), `git ["-C"`) || !strings.Contains(err.Error(), fmt.Sprintf(`"checkout" "%s"`, repo.Commit)) || !strings.Contains(err.Error(), "branch not found") {
+ t.Errorf("Expected checkout error, got %v", err)
+ }
+ })
+}
+
+func TestCloneRepo(t *testing.T) {
+ tempCodeDir := t.TempDir()
+ localPath := filepath.Join(tempCodeDir, "github/owner/new-repo")
+
+ cfg := &config.Config{
+ Servers: map[string]*config.Server{
+ "github": {
+ Name: "github",
+ CloneURL: "https://github.com",
+ },
+ },
+ }
+ repo := &config.Repository{
+ Server: "github", Owner: "owner", Name: "new-repo",
+ Branch: "main", Commit: "mocksha1234567890123456789012345678901234567890",
+ }
+
+ t.Run("successful clone and checkout", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "clone", "https://github.com/owner/new-repo.git", localPath}, stdout: ""},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "checkout", repo.Commit}, stdout: ""},
+ })
+
+ err := cloneRepo(cfg, repo, localPath, false)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+ if _, err := os.Stat(path.Dir(localPath)); os.IsNotExist(err) {
+ t.Errorf("Expected cloned directory to exist at %s", localPath)
+ }
+ })
+
+ t.Run("no server config found", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{})
+ repo.Server = "nonexistent"
+ err := cloneRepo(cfg, repo, localPath, false)
+ if err == nil {
+ t.Fatal("Expected error for missing server config, got nil")
+ }
+ if !strings.Contains(err.Error(), "no server config found for 'nonexistent'") {
+ t.Errorf("Expected 'no server config' error, got %v", err)
+ }
+ repo.Server = "github"
+ })
+
+ t.Run("clone command fails", func(t *testing.T) {
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "clone", "https://github.com/owner/new-repo.git", localPath}, stderr: "fatal: clone failed\n", err: fmt.Errorf("exit status 1")},
+ })
+
+ err := cloneRepo(cfg, repo, localPath, false)
+ if err == nil {
+ t.Fatal("Expected error for clone failure, got nil")
+ }
+ if !strings.Contains(err.Error(), `git ["clone"`) || !strings.Contains(err.Error(), "clone failed") {
+ t.Errorf("Expected clone failed error, got %v", err)
+ }
+ })
+}
+
+func TestListWithMaxDepth(t *testing.T) {
+ tempRoot := t.TempDir()
+
+ os.MkdirAll(filepath.Join(tempRoot, "dir1", "dir1_1", "dir_1_1_1", "dir_1_1_1_1"), 0755)
+ os.MkdirAll(filepath.Join(tempRoot, "dir1", "dir1_1", "dir_1_1_2"), 0755)
+ os.MkdirAll(filepath.Join(tempRoot, "dir2", "dir2_1", "dir_2_1_1"), 0755)
+ os.MkdirAll(filepath.Join(tempRoot, "dir2", "dir3_1", "dir_3_1_1"), 0755)
+ os.MkdirAll(filepath.Join(tempRoot, "dir2", "dir3_2"), 0755)
+ os.MkdirAll(filepath.Join(tempRoot, "dir3"), 0755)
+
+ t.Run("max depth 1", func(t *testing.T) {
+ paths, err := listWithMaxDepth(tempRoot, 1)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+ expected := map[string]bool{"dir1": true, "dir2": true, "dir3": true}
+ if !reflect.DeepEqual(paths, expected) {
+ t.Errorf("Expected %+v paths, got %+v", expected, paths)
+ }
+ })
+
+ t.Run("max depth 2", func(t *testing.T) {
+ paths, err := listWithMaxDepth(tempRoot, 2)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+ expected := map[string]bool{
+ "dir1/dir1_1": true, "dir2/dir2_1": true, "dir2/dir3_1": true, "dir2/dir3_2": true, "dir3": true,
+ }
+ if !reflect.DeepEqual(paths, expected) {
+ t.Errorf("Expected %+v paths, got %+v", expected, paths)
+ }
+ })
+
+ t.Run("non-existent root", func(t *testing.T) {
+ _, err := listWithMaxDepth("/non/existent/path", 1)
+ if err == nil || !strings.Contains(err.Error(), "no such file or directory") {
+ t.Errorf("Expected 'no such file or directory' error, got %v", err)
+ }
+ })
+}
+
+func TestSyncRepos(t *testing.T) {
+ log.SetOutput(io.Discard)
+ defer log.SetOutput(os.Stderr)
+
+ tempCodeDir := t.TempDir()
+ tempManifestPath := filepath.Join(t.TempDir(), "manifest.json")
+
+ cfg := &config.Config{
+ CodeDir: tempCodeDir,
+ ManifestPath: tempManifestPath,
+ Servers: map[string]*config.Server{
+ "github": {
+ Name: "github",
+ CloneURL: "https://github.com",
+ },
+ },
+ }
+
+ t.Run("remove orphaned repo", func(t *testing.T) {
+ orphanPath := filepath.Join(tempCodeDir, "server/owner/orphan-repo")
+ os.MkdirAll(orphanPath, 0755)
+ os.WriteFile(filepath.Join(orphanPath, "README.md"), []byte(""), 0644)
+
+ createDummyManifest(t, tempManifestPath, []*config.Repository{})
+ setMockGitCommand(t, []mockCmd{})
+
+ err := SyncRepos(cfg, false)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+ if _, err := os.Stat(orphanPath); !os.IsNotExist(err) {
+ t.Errorf("Expected orphan repo %s to be removed, but it still exists", orphanPath)
+ }
+ })
+
+ t.Run("corrupt git index", func(t *testing.T) {
+ repo := &config.Repository{
+ Server: "github", Owner: "owner", Name: "corrupt-repo", Branch: "main", Commit: "corruptsha",
+ }
+ localPath := filepath.Join(tempCodeDir, repo.RepoDir())
+ os.MkdirAll(filepath.Join(localPath, ".git"), 0755)
+
+ createDummyManifest(t, tempManifestPath, []*config.Repository{repo})
+ setMockGitCommand(t, []mockCmd{
+ {expectedCmdArgs: []string{"git", "clone", "https://github.com/owner/corrupt-repo.git", localPath}, stdout: ""},
+ {expectedCmdArgs: []string{"git", "-C", localPath, "checkout", repo.Commit}, stdout: ""},
+ })
+
+ err := SyncRepos(cfg, false)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(localPath)); !os.IsNotExist(err) {
+ t.Errorf("Expected corrupt repo to be deleted before cloning: %v", err)
+ }
+ })
+
+ t.Run("manifest read error", func(t *testing.T) {
+ cfg.ManifestPath = filepath.Join(t.TempDir(), "nonexistent_manifest.json")
+ err := SyncRepos(cfg, false)
+ if err == nil || !strings.Contains(err.Error(), "failed to read manifest") && !strings.Contains(err.Error(), "no such file or directory") {
+ t.Errorf("Expected manifest read error, got %v", err)
+ }
+ cfg.ManifestPath = tempManifestPath
+ })
+}
diff --git a/cmd/csupdater/updater.go b/cmd/csupdater/updater.go
new file mode 100644
index 0000000..627aeb4
--- /dev/null
+++ b/cmd/csupdater/updater.go
@@ -0,0 +1,178 @@
+package main
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path"
+ "reflect"
+ "time"
+
+ "github.com/freva/codesearch/internal/config"
+)
+
+// AppArgs holds the parsed command-line arguments.
+type AppArgs struct {
+ ConfigFile string
+ DoManifest bool
+ DoSync bool
+ DoIndex bool
+ Verbose bool
+ ExitEarly bool
+ HelpConfig bool
+}
+
+func main() {
+ args := AppArgs{}
+ flag.StringVar(&args.ConfigFile, "config", "", "Path to config file (required).")
+ flag.BoolVar(&args.DoManifest, "manifest", false, "Update the manifest (only).")
+ flag.BoolVar(&args.DoSync, "sync", false, "Synchronize git repos (only).")
+ flag.BoolVar(&args.DoIndex, "index", false, "Update the search indices (only).")
+ flag.BoolVar(&args.Verbose, "verbose", false, "Enable verbose output.")
+ flag.BoolVar(&args.ExitEarly, "exit-early", false, "Skips sync & index if manifest was unchanged.")
+ flag.BoolVar(&args.HelpConfig, "help-config", false, "Show help for the config file format.")
+
+ flag.Usage = func() {
+ _, _ = fmt.Fprintf(os.Stderr, `Usage: updater [OPTION...]
+Update the manifest, synchronize the git repos, and update the indices.
+
+Options:
+`)
+ flag.PrintDefaults()
+ }
+
+ flag.Parse()
+
+ if args.HelpConfig {
+ fmt.Println(config.Help())
+ return
+ }
+ if args.ConfigFile == "" {
+ log.Fatal("Error: --config flag is required. See --help for usage.")
+ }
+
+ // If no specific action is chosen, default to running all actions.
+ if !args.DoManifest && !args.DoSync && !args.DoIndex {
+ args.DoManifest = true
+ args.DoSync = true
+ args.DoIndex = true
+ }
+
+ err := run(args)
+ if err != nil {
+ log.Printf("ERROR: %v", err)
+ os.Exit(1)
+ }
+}
+
+func run(args AppArgs) error {
+ cfg, err := config.ReadConfig(args.ConfigFile)
+ if err != nil {
+ return fmt.Errorf("could not parse cfg file: %w", err)
+ }
+
+ paths := []string{path.Dir(cfg.CodeIndexPath), path.Dir(cfg.FileIndexPath), path.Dir(cfg.ManifestPath), cfg.CodeDir}
+ for _, p := range paths {
+ if err := os.MkdirAll(p, 0755); err != nil {
+ return fmt.Errorf("could not create directory '%s': %w", p, err)
+ }
+ }
+
+ if args.DoManifest {
+ var manifestChanged, err = updateManifest(cfg, args.Verbose)
+ if err != nil {
+ return fmt.Errorf("manifest update failed: %w", err)
+ }
+ if args.ExitEarly && !manifestChanged {
+ return nil
+ }
+ }
+
+ if args.DoSync {
+ if err := SyncRepos(cfg, args.Verbose); err != nil {
+ return fmt.Errorf("repository sync failed: %w", err)
+ }
+ }
+
+ if args.DoIndex {
+ if err := UpdateIndices(cfg, args.Verbose); err != nil {
+ return fmt.Errorf("indexing failed: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func updateManifest(cfg *config.Config, verbose bool) (bool, error) {
+ start := time.Now()
+ repos, err := GetAllRepositories(cfg, verbose)
+ if err != nil {
+ return false, fmt.Errorf("could not fetch repositories: %w", err)
+ }
+
+ servers := make(map[string]string)
+ for name, server := range cfg.Servers {
+ servers[name] = server.WebURL
+ }
+ reposByPrefix := make(map[string]*config.Repository)
+ for _, repo := range repos {
+ reposByPrefix[repo.RepoDir()] = &repo
+ }
+
+ newManifest := &config.Manifest{
+ Servers: servers,
+ Repositories: reposByPrefix,
+ UpdatedAt: time.Now(),
+ }
+
+ changed := true
+ oldManifest, err := config.ReadManifest(cfg.ManifestPath)
+ if err == nil {
+ serversEqual := reflect.DeepEqual(oldManifest.Servers, newManifest.Servers)
+ reposEqual := reflect.DeepEqual(oldManifest.Repositories, newManifest.Repositories)
+ changed = !(serversEqual && reposEqual)
+ }
+
+ serialized, err := json.MarshalIndent(newManifest, "", " ")
+ if err != nil {
+ return false, fmt.Errorf("could not marshal manifest: %w", err)
+ }
+ if err := atomicWriteFile(cfg.ManifestPath, serialized); err != nil {
+ return false, fmt.Errorf("could not write manifest file: %w", err)
+ }
+ if changed {
+ log.Printf("Found %d repositories for %d servers in %s.\n", len(repos), len(cfg.Servers), time.Since(start).Round(10*time.Millisecond))
+ }
+ return changed, nil
+}
+
+func atomicWriteFile(filePath string, data []byte) error {
+ tmpPath := path.Join(path.Dir(filePath), path.Base(filePath)+".tmp")
+ tmpFile, err := os.Create(tmpPath)
+ if err != nil {
+ return fmt.Errorf("failed to create temporary file: %w", err)
+ }
+
+ defer func() {
+ // If the rename operation succeeds, this remove will fail, which is fine.
+ // If the rename fails, this will clean up the lingering temp file.
+ _ = os.Remove(tmpPath)
+ }()
+
+ if _, err := tmpFile.Write(data); err != nil {
+ _ = tmpFile.Close()
+ return fmt.Errorf("failed to write data to temporary file: %w", err)
+ }
+
+ if err := tmpFile.Close(); err != nil {
+ return fmt.Errorf("failed to close temporary file: %w", err)
+ }
+
+ if err := os.Rename(tmpPath, filePath); err != nil {
+ return fmt.Errorf("failed to rename temporary file to final path: %w", err)
+ }
+
+ return nil
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..1770217
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,7 @@
+.DS_STORE
+.idea
+*.iml
+.vscode
+
+build
+node_modules/
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000..713ce20
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "singleQuote": true,
+ "tabWidth": 2,
+ "printWidth": 120,
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts
new file mode 100644
index 0000000..2aa014e
--- /dev/null
+++ b/frontend/eslint.config.ts
@@ -0,0 +1,51 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import reactX from 'eslint-plugin-react-x';
+import reactDom from 'eslint-plugin-react-dom';
+import tseslint from 'typescript-eslint';
+import { globalIgnores } from 'eslint/config';
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { defineConfig } from 'eslint/config';
+
+export default defineConfig([
+ globalIgnores(['build', 'node_modules']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ ...tseslint.configs.recommendedTypeChecked,
+ ...tseslint.configs.strictTypeChecked,
+ ...tseslint.configs.stylisticTypeChecked,
+ eslintPluginPrettierRecommended,
+ ],
+ plugins: {
+ 'react-hooks': {
+ rules: reactHooks.rules,
+ },
+ 'react-refresh': reactRefresh,
+ 'react-x': reactX,
+ 'react-dom': reactDom,
+ },
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ project: ['./tsconfig.json'],
+ tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
+ },
+ },
+ rules: {
+ '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
+ '@typescript-eslint/explicit-function-return-type': 'error',
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/no-unnecessary-condition': 'off',
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
+ '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
+ 'react-x/no-array-index-key': 'off',
+ },
+ },
+]);
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100755
index 0000000..2ff10ea
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Code Search
+
+
+
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..ea7de20
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "code-search",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "build": "vite build --outDir build/",
+ "test": "CI=true vitest",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "lodash-es": "^4.17.23",
+ "prismjs": "^1.30.0",
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "react-hook-form": "^7.71.1",
+ "react-icons": "^5.5.0",
+ "react-router-dom": "^7.13.0",
+ "use-context-selector": "^2.0.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^25.0.10",
+ "@types/prismjs": "^1.26.5",
+ "@types/react": "^19.2.9",
+ "@types/react-dom": "^19.2.3",
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
+ "@typescript-eslint/parser": "^8.53.1",
+ "@vitejs/plugin-react": "^5.1.2",
+ "eslint": "^9.39.2",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.5",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-dom": "^2.7.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.26",
+ "eslint-plugin-react-x": "^2.7.4",
+ "globals": "^17.1.0",
+ "happy-dom": "^20.3.7",
+ "prettier": "^3.8.1",
+ "prettier-plugin-tailwindcss": "^0.7.2",
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.53.1",
+ "vite": "^7.3.1",
+ "vitest": "^4.0.18"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
new file mode 100644
index 0000000..94110e4
--- /dev/null
+++ b/frontend/pnpm-lock.yaml
@@ -0,0 +1,4325 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ lodash-es:
+ specifier: ^4.17.23
+ version: 4.17.23
+ prismjs:
+ specifier: ^1.30.0
+ version: 1.30.0
+ react:
+ specifier: ^19.2.3
+ version: 19.2.3
+ react-dom:
+ specifier: ^19.2.3
+ version: 19.2.3(react@19.2.3)
+ react-hook-form:
+ specifier: ^7.71.1
+ version: 7.71.1(react@19.2.3)
+ react-icons:
+ specifier: ^5.5.0
+ version: 5.5.0(react@19.2.3)
+ react-router-dom:
+ specifier: ^7.13.0
+ version: 7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ use-context-selector:
+ specifier: ^2.0.0
+ version: 2.0.0(react@19.2.3)(scheduler@0.27.0)
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.39.2
+ version: 9.39.2
+ '@tailwindcss/vite':
+ specifier: ^4.1.18
+ version: 4.1.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)))
+ '@types/lodash-es':
+ specifier: ^4.17.12
+ version: 4.17.12
+ '@types/node':
+ specifier: ^25.0.10
+ version: 25.0.10
+ '@types/prismjs':
+ specifier: ^1.26.5
+ version: 1.26.5
+ '@types/react':
+ specifier: ^19.2.9
+ version: 19.2.9
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.9)
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^8.53.1
+ version: 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/parser':
+ specifier: ^8.53.1
+ version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@vitejs/plugin-react':
+ specifier: ^5.1.2
+ version: 5.1.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)))
+ eslint:
+ specifier: ^9.39.2
+ version: 9.39.2(jiti@2.6.1)
+ eslint-config-prettier:
+ specifier: ^10.1.8
+ version: 10.1.8(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-prettier:
+ specifier: ^5.5.5
+ version: 5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1)
+ eslint-plugin-react:
+ specifier: ^7.37.5
+ version: 7.37.5(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-react-dom:
+ specifier: ^2.7.4
+ version: 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint-plugin-react-hooks:
+ specifier: ^7.0.1
+ version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.26
+ version: 0.4.26(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-react-x:
+ specifier: ^2.7.4
+ version: 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ globals:
+ specifier: ^17.1.0
+ version: 17.1.0
+ happy-dom:
+ specifier: ^20.3.7
+ version: 20.3.7
+ prettier:
+ specifier: ^3.8.1
+ version: 3.8.1
+ prettier-plugin-tailwindcss:
+ specifier: ^0.7.2
+ version: 0.7.2(prettier@3.8.1)
+ tailwindcss:
+ specifier: ^4.1.18
+ version: 4.1.18
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+ typescript-eslint:
+ specifier: ^8.53.1
+ version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))
+ vitest:
+ specifier: ^4.0.18
+ version: 4.0.18(@types/node@25.0.10)(happy-dom@20.3.7)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))
+
+packages:
+
+ '@babel/code-frame@7.28.6':
+ resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.28.6':
+ resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.28.6':
+ resolution: {integrity: sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.28.6':
+ resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.28.0':
+ resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.28.6':
+ resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.28.6':
+ resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.28.6':
+ resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.28.6':
+ resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.27.2':
+ resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.27.2':
+ resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.27.2':
+ resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.27.2':
+ resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.27.2':
+ resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.27.2':
+ resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.27.2':
+ resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.27.2':
+ resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.27.2':
+ resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.27.2':
+ resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.27.2':
+ resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.27.2':
+ resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.27.2':
+ resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.27.2':
+ resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.27.2':
+ resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.27.2':
+ resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.27.2':
+ resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.27.2':
+ resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.2':
+ resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.27.2':
+ resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.2':
+ resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.27.2':
+ resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.27.2':
+ resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.27.2':
+ resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.27.2':
+ resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.27.2':
+ resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint-react/ast@2.7.4':
+ resolution: {integrity: sha512-es148MgD+yXVT+OW2SKgUZeVq5xIQ3FESjnY6A1XMEO92neDxij8Suo1CTDKurMw4jMHELmB7CPhg/FqsfvnJg==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@eslint-react/core@2.7.4':
+ resolution: {integrity: sha512-L2LrKNFqUPhhChPZyHz1ak11GQAxGRRrGBw0q9sNqm9taPO1Eu/U8wrcO/X5jhYT3orROZklCl0z+q8pxM3A/g==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@eslint-react/eff@2.7.4':
+ resolution: {integrity: sha512-L+ZU/m7UudB7fYaMLrNgt700gjFJ9Wa4HQxe4UXXd6z2LecJbYEXo2Z+dU/e5I21/jxtH+iq+bnZwCxh3SaRtA==}
+ engines: {node: '>=20.19.0'}
+
+ '@eslint-react/shared@2.7.4':
+ resolution: {integrity: sha512-at8Ib51JJl1GJy+ylRDG3zv64FD2V89sofQ9iemu6DXkya2ZSE5dcO2EN7FmEj6CyYS/YRu3XlJ3dXHShDYPLg==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@eslint-react/var@2.7.4':
+ resolution: {integrity: sha512-RdcX5j/3EvI+qchordszVD3pjCAV+3+vNEztTEuZB6G1Le3ulQaLsQGfLP70INut9IyZaZ56hyC0bwfgqIFjQA==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@eslint/config-array@0.21.1':
+ resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.4.2':
+ resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.17.0':
+ resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.3':
+ resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.39.2':
+ resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.7':
+ resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.4.1':
+ resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.7':
+ resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@pkgr/core@0.2.9':
+ resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+
+ '@rolldown/pluginutils@1.0.0-beta.53':
+ resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
+
+ '@rollup/rollup-android-arm-eabi@4.56.0':
+ resolution: {integrity: sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.56.0':
+ resolution: {integrity: sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.56.0':
+ resolution: {integrity: sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.56.0':
+ resolution: {integrity: sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.56.0':
+ resolution: {integrity: sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.56.0':
+ resolution: {integrity: sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.56.0':
+ resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.56.0':
+ resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.56.0':
+ resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.56.0':
+ resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.56.0':
+ resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.56.0':
+ resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.56.0':
+ resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.56.0':
+ resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.56.0':
+ resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.56.0':
+ resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.56.0':
+ resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.56.0':
+ resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.56.0':
+ resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.56.0':
+ resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.56.0':
+ resolution: {integrity: sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.56.0':
+ resolution: {integrity: sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.56.0':
+ resolution: {integrity: sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.56.0':
+ resolution: {integrity: sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.56.0':
+ resolution: {integrity: sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@tailwindcss/node@4.1.18':
+ resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.18':
+ resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.18':
+ resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.18':
+ resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.18':
+ resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
+ resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
+ resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
+ resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
+ resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.18':
+ resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.18':
+ resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
+ resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
+ resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.18':
+ resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.18':
+ resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/lodash-es@4.17.12':
+ resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+
+ '@types/lodash@4.17.23':
+ resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==}
+
+ '@types/node@25.0.10':
+ resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
+
+ '@types/prismjs@1.26.5':
+ resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
+
+ '@types/react-dom@19.2.3':
+ resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.9':
+ resolution: {integrity: sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==}
+
+ '@types/whatwg-mimetype@3.0.2':
+ resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==}
+
+ '@types/ws@8.18.1':
+ resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
+ '@typescript-eslint/eslint-plugin@8.53.1':
+ resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.53.1
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/parser@8.53.1':
+ resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/project-service@8.53.1':
+ resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/scope-manager@8.53.1':
+ resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.53.1':
+ resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/type-utils@8.53.1':
+ resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/types@8.53.1':
+ resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.53.1':
+ resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/utils@8.53.1':
+ resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ '@typescript-eslint/visitor-keys@8.53.1':
+ resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@5.1.2':
+ resolution: {integrity: sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+ '@vitest/expect@4.0.18':
+ resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
+
+ '@vitest/mocker@4.0.18':
+ resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.0.18':
+ resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
+
+ '@vitest/runner@4.0.18':
+ resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
+
+ '@vitest/snapshot@4.0.18':
+ resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
+
+ '@vitest/spy@4.0.18':
+ resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
+
+ '@vitest/utils@4.0.18':
+ resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
+ array-includes@3.1.9:
+ resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.findlast@1.2.5:
+ resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flat@1.3.3:
+ resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flatmap@1.3.3:
+ resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.tosorted@1.1.4:
+ resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
+ engines: {node: '>= 0.4'}
+
+ arraybuffer.prototype.slice@1.0.4:
+ resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+ engines: {node: '>= 0.4'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ async-function@1.0.0:
+ resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+ engines: {node: '>= 0.4'}
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ baseline-browser-mapping@2.9.18:
+ resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==}
+ hasBin: true
+
+ birecord@0.1.1:
+ resolution: {integrity: sha512-VUpsf/qykW0heRlC8LooCq28Kxn3mAqKohhDG/49rrsQ1dT1CXyj/pgXS+5BSRzFTR/3DyIBOqQOrGyZOh71Aw==}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ browserslist@4.28.1:
+ resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001766:
+ resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ compare-versions@6.1.1:
+ resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ data-view-buffer@1.0.2:
+ resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-length@1.0.2:
+ resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-offset@1.0.1:
+ resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+ engines: {node: '>= 0.4'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ doctrine@2.1.0:
+ resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
+ engines: {node: '>=0.10.0'}
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ electron-to-chromium@1.5.278:
+ resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==}
+
+ enhanced-resolve@5.18.4:
+ resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
+ engines: {node: '>=10.13.0'}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ es-abstract@1.24.1:
+ resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==}
+ engines: {node: '>= 0.4'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-iterator-helpers@1.2.2:
+ resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ es-shim-unscopables@1.1.0:
+ resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
+ engines: {node: '>= 0.4'}
+
+ es-to-primitive@1.3.0:
+ resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+ engines: {node: '>= 0.4'}
+
+ esbuild@0.27.2:
+ resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-config-prettier@10.1.8:
+ resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+
+ eslint-plugin-prettier@5.5.5:
+ resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ '@types/eslint': '>=8.0.0'
+ eslint: '>=8.0.0'
+ eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
+ prettier: '>=3.0.0'
+ peerDependenciesMeta:
+ '@types/eslint':
+ optional: true
+ eslint-config-prettier:
+ optional: true
+
+ eslint-plugin-react-dom@2.7.4:
+ resolution: {integrity: sha512-bQb4kkls+TEqkkPib6r5D2r2+WFeSSHBxaHDcpOXVFybz+gMenz9l+bUbQAShzPJVuzn+z65jmt5UEw06rEv9w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ eslint-plugin-react-hooks@7.0.1:
+ resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.26:
+ resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-plugin-react-x@2.7.4:
+ resolution: {integrity: sha512-IZPvMvE3iHxWzKIIfkb0Fcogxr++XHMh6dSjBcFVpmQmzLV4MpwFIe4PUgV8kM0uF/ULfAJ3oVyti3Ydj04yzw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ eslint-plugin-react@7.37.5:
+ resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.39.2:
+ resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-diff@1.3.0:
+ resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ function.prototype.name@1.1.8:
+ resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
+ engines: {node: '>= 0.4'}
+
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
+ generator-function@2.0.1:
+ resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
+ engines: {node: '>= 0.4'}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-symbol-description@1.1.0:
+ resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@17.1.0:
+ resolution: {integrity: sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw==}
+ engines: {node: '>=18'}
+
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ happy-dom@20.3.7:
+ resolution: {integrity: sha512-sb5IzoRl1WJKsUSRe+IloJf3z1iDq5PQ7Yk/ULMsZ5IAQEs9ZL7RsFfiKBXU7nK9QmO+iz0e59EH8r8jexTZ/g==}
+ engines: {node: '>=20.0.0'}
+
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.2.0:
+ resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hermes-estree@0.25.1:
+ resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+
+ hermes-parser@0.25.1:
+ resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-async-function@2.1.1:
+ resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.16.1:
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+ engines: {node: '>= 0.4'}
+
+ is-data-view@1.0.2:
+ resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-finalizationregistry@1.1.1:
+ resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+ engines: {node: '>= 0.4'}
+
+ is-generator-function@1.1.2:
+ resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
+ engines: {node: '>= 0.4'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-immutable-type@5.0.1:
+ resolution: {integrity: sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==}
+ peerDependencies:
+ eslint: '*'
+ typescript: '>=4.7.4'
+
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-negative-zero@2.0.3:
+ resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+ engines: {node: '>= 0.4'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakref@1.1.1:
+ resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ iterator.prototype@1.1.5:
+ resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
+ engines: {node: '>= 0.4'}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsx-ast-utils@3.3.5:
+ resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
+ engines: {node: '>=4.0'}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.30.2:
+ resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.30.2:
+ resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.30.2:
+ resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.30.2:
+ resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.30.2:
+ resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.30.2:
+ resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.30.2:
+ resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.30.2:
+ resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.30.2:
+ resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.30.2:
+ resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.30.2:
+ resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.30.2:
+ resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash-es@4.17.23:
+ resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.27:
+ resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
+ object.entries@1.1.9:
+ resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
+ engines: {node: '>= 0.4'}
+
+ object.fromentries@2.0.8:
+ resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+ engines: {node: '>= 0.4'}
+
+ object.values@1.2.1:
+ resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
+ engines: {node: '>= 0.4'}
+
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ own-keys@1.0.1:
+ resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+ engines: {node: '>= 0.4'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prettier-linter-helpers@1.0.1:
+ resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
+ engines: {node: '>=6.0.0'}
+
+ prettier-plugin-tailwindcss@0.7.2:
+ resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
+ engines: {node: '>=20.19'}
+ peerDependencies:
+ '@ianvs/prettier-plugin-sort-imports': '*'
+ '@prettier/plugin-hermes': '*'
+ '@prettier/plugin-oxc': '*'
+ '@prettier/plugin-pug': '*'
+ '@shopify/prettier-plugin-liquid': '*'
+ '@trivago/prettier-plugin-sort-imports': '*'
+ '@zackad/prettier-plugin-twig': '*'
+ prettier: ^3.0
+ prettier-plugin-astro: '*'
+ prettier-plugin-css-order: '*'
+ prettier-plugin-jsdoc: '*'
+ prettier-plugin-marko: '*'
+ prettier-plugin-multiline-arrays: '*'
+ prettier-plugin-organize-attributes: '*'
+ prettier-plugin-organize-imports: '*'
+ prettier-plugin-sort-imports: '*'
+ prettier-plugin-svelte: '*'
+ peerDependenciesMeta:
+ '@ianvs/prettier-plugin-sort-imports':
+ optional: true
+ '@prettier/plugin-hermes':
+ optional: true
+ '@prettier/plugin-oxc':
+ optional: true
+ '@prettier/plugin-pug':
+ optional: true
+ '@shopify/prettier-plugin-liquid':
+ optional: true
+ '@trivago/prettier-plugin-sort-imports':
+ optional: true
+ '@zackad/prettier-plugin-twig':
+ optional: true
+ prettier-plugin-astro:
+ optional: true
+ prettier-plugin-css-order:
+ optional: true
+ prettier-plugin-jsdoc:
+ optional: true
+ prettier-plugin-marko:
+ optional: true
+ prettier-plugin-multiline-arrays:
+ optional: true
+ prettier-plugin-organize-attributes:
+ optional: true
+ prettier-plugin-organize-imports:
+ optional: true
+ prettier-plugin-sort-imports:
+ optional: true
+ prettier-plugin-svelte:
+ optional: true
+
+ prettier@3.8.1:
+ resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.3:
+ resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
+ peerDependencies:
+ react: ^19.2.3
+
+ react-hook-form@7.71.1:
+ resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-icons@5.5.0:
+ resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
+ peerDependencies:
+ react: '*'
+
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
+ react-router-dom@7.13.0:
+ resolution: {integrity: sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.13.0:
+ resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ react@19.2.3:
+ resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
+ engines: {node: '>=0.10.0'}
+
+ reflect.getprototypeof@1.0.10:
+ resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+ engines: {node: '>= 0.4'}
+
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve@2.0.0-next.5:
+ resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
+ hasBin: true
+
+ rollup@4.56.0:
+ resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ safe-array-concat@1.1.3:
+ resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
+ engines: {node: '>=0.4'}
+
+ safe-push-apply@1.0.0:
+ resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.3:
+ resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@3.10.0:
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
+ string-ts@2.3.1:
+ resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==}
+
+ string.prototype.matchall@4.0.12:
+ resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.repeat@1.0.0:
+ resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
+
+ string.prototype.trim@1.2.10:
+ resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.9:
+ resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ sugarss@5.0.1:
+ resolution: {integrity: sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==}
+ engines: {node: '>=18.0'}
+ peerDependencies:
+ postcss: ^8.3.3
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ synckit@0.11.12:
+ resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+
+ tailwindcss@4.1.18:
+ resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
+
+ tapable@2.3.0:
+ resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
+ engines: {node: '>=6'}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.0.2:
+ resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.0.3:
+ resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
+ engines: {node: '>=14.0.0'}
+
+ ts-api-utils@2.4.0:
+ resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ ts-declaration-location@1.0.7:
+ resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==}
+ peerDependencies:
+ typescript: '>=4.0.0'
+
+ ts-pattern@5.9.0:
+ resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typed-array-buffer@1.0.3:
+ resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-length@1.0.3:
+ resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-offset@1.0.4:
+ resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-length@1.0.7:
+ resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
+ engines: {node: '>= 0.4'}
+
+ typescript-eslint@8.53.1:
+ resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <6.0.0'
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ unbox-primitive@1.1.0:
+ resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+ engines: {node: '>= 0.4'}
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-context-selector@2.0.0:
+ resolution: {integrity: sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==}
+ peerDependencies:
+ react: '>=18.0.0'
+ scheduler: '>=0.19.0'
+
+ vite@7.3.1:
+ resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ 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
+
+ vitest@4.0.18:
+ resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.0.18
+ '@vitest/browser-preview': 4.0.18
+ '@vitest/browser-webdriverio': 4.0.18
+ '@vitest/ui': 4.0.18
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ whatwg-mimetype@3.0.0:
+ resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+ engines: {node: '>=12'}
+
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-builtin-type@1.2.1:
+ resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.20:
+ resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
+ engines: {node: '>= 0.4'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ ws@8.19.0:
+ resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod-validation-error@4.0.2:
+ resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^3.25.0 || ^4.0.0
+
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
+snapshots:
+
+ '@babel/code-frame@7.28.6':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.28.6': {}
+
+ '@babel/core@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.28.6
+ '@babel/generator': 7.28.6
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
+ '@babel/helpers': 7.28.6
+ '@babel/parser': 7.28.6
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.28.6
+ '@babel/types': 7.28.6
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.28.6':
+ dependencies:
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.1
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.28.0': {}
+
+ '@babel/helper-module-imports@7.28.6':
+ dependencies:
+ '@babel/traverse': 7.28.6
+ '@babel/types': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)':
+ dependencies:
+ '@babel/core': 7.28.6
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.28.6
+
+ '@babel/parser@7.28.6':
+ dependencies:
+ '@babel/types': 7.28.6
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.6)':
+ dependencies:
+ '@babel/core': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.6)':
+ dependencies:
+ '@babel/core': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.28.6
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
+
+ '@babel/traverse@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.28.6
+ '@babel/generator': 7.28.6
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.28.6
+ '@babel/template': 7.28.6
+ '@babel/types': 7.28.6
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.28.6':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@esbuild/aix-ppc64@0.27.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/android-arm@0.27.2':
+ optional: true
+
+ '@esbuild/android-x64@0.27.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.2':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))':
+ dependencies:
+ eslint: 9.39.2(jiti@2.6.1)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint-react/ast@2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-react/eff': 2.7.4
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ string-ts: 2.3.1
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint-react/core@2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-react/ast': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/eff': 2.7.4
+ '@eslint-react/shared': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/var': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ birecord: 0.1.1
+ eslint: 9.39.2(jiti@2.6.1)
+ ts-pattern: 5.9.0
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint-react/eff@2.7.4': {}
+
+ '@eslint-react/shared@2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-react/eff': 2.7.4
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ ts-pattern: 5.9.0
+ typescript: 5.9.3
+ zod: 4.3.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint-react/var@2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-react/ast': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/eff': 2.7.4
+ '@eslint-react/shared': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ ts-pattern: 5.9.0
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-array@0.21.1':
+ dependencies:
+ '@eslint/object-schema': 2.1.7
+ debug: 4.4.3
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.4.2':
+ dependencies:
+ '@eslint/core': 0.17.0
+
+ '@eslint/core@0.17.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.3':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.3
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.1
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.39.2': {}
+
+ '@eslint/object-schema@2.1.7': {}
+
+ '@eslint/plugin-kit@0.4.1':
+ dependencies:
+ '@eslint/core': 0.17.0
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.7':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@pkgr/core@0.2.9': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.53': {}
+
+ '@rollup/rollup-android-arm-eabi@4.56.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.56.0':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.56.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.56.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.56.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.56.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.56.0':
+ optional: true
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@tailwindcss/node@4.1.18':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.18.4
+ jiti: 2.6.1
+ lightningcss: 1.30.2
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.1.18
+
+ '@tailwindcss/oxide-android-arm64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.18':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.18
+ '@tailwindcss/oxide-darwin-arm64': 4.1.18
+ '@tailwindcss/oxide-darwin-x64': 4.1.18
+ '@tailwindcss/oxide-freebsd-x64': 4.1.18
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.18
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.18
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.18
+ '@tailwindcss/oxide-wasm32-wasi': 4.1.18
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.18
+
+ '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)))':
+ dependencies:
+ '@tailwindcss/node': 4.1.18
+ '@tailwindcss/oxide': 4.1.18
+ tailwindcss: 4.1.18
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.28.6
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.28.6
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/lodash-es@4.17.12':
+ dependencies:
+ '@types/lodash': 4.17.23
+
+ '@types/lodash@4.17.23': {}
+
+ '@types/node@25.0.10':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/prismjs@1.26.5': {}
+
+ '@types/react-dom@19.2.3(@types/react@19.2.9)':
+ dependencies:
+ '@types/react': 19.2.9
+
+ '@types/react@19.2.9':
+ dependencies:
+ csstype: 3.2.3
+
+ '@types/whatwg-mimetype@3.0.2': {}
+
+ '@types/ws@8.18.1':
+ dependencies:
+ '@types/node': 25.0.10
+
+ '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.53.1
+ eslint: 9.39.2(jiti@2.6.1)
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.53.1
+ debug: 4.4.3
+ eslint: 9.39.2(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.53.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.53.1
+ debug: 4.4.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.53.1':
+ dependencies:
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/visitor-keys': 8.53.1
+
+ '@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)':
+ dependencies:
+ typescript: 5.9.3
+
+ '@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ debug: 4.4.3
+ eslint: 9.39.2(jiti@2.6.1)
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.53.1': {}
+
+ '@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/visitor-keys': 8.53.1
+ debug: 4.4.3
+ minimatch: 9.0.5
+ semver: 7.7.3
+ tinyglobby: 0.2.15
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.53.1':
+ dependencies:
+ '@typescript-eslint/types': 8.53.1
+ eslint-visitor-keys: 4.2.1
+
+ '@vitejs/plugin-react@5.1.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)))':
+ dependencies:
+ '@babel/core': 7.28.6
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.6)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.6)
+ '@rolldown/pluginutils': 1.0.0-beta.53
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/expect@4.0.18':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.0.18
+ '@vitest/utils': 4.0.18
+ chai: 6.2.2
+ tinyrainbow: 3.0.3
+
+ '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)))':
+ dependencies:
+ '@vitest/spy': 4.0.18
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))
+
+ '@vitest/pretty-format@4.0.18':
+ dependencies:
+ tinyrainbow: 3.0.3
+
+ '@vitest/runner@4.0.18':
+ dependencies:
+ '@vitest/utils': 4.0.18
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.0.18':
+ dependencies:
+ '@vitest/pretty-format': 4.0.18
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.0.18': {}
+
+ '@vitest/utils@4.0.18':
+ dependencies:
+ '@vitest/pretty-format': 4.0.18
+ tinyrainbow: 3.0.3
+
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
+ array-includes@3.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ is-string: 1.1.1
+ math-intrinsics: 1.1.0
+
+ array.prototype.findlast@1.2.5:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flat@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flatmap@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.tosorted@1.1.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-shim-unscopables: 1.1.0
+
+ arraybuffer.prototype.slice@1.0.4:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ is-array-buffer: 3.0.5
+
+ assertion-error@2.0.1: {}
+
+ async-function@1.0.0: {}
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
+ balanced-match@1.0.2: {}
+
+ baseline-browser-mapping@2.9.18: {}
+
+ birecord@0.1.1: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ browserslist@4.28.1:
+ dependencies:
+ baseline-browser-mapping: 2.9.18
+ caniuse-lite: 1.0.30001766
+ electron-to-chromium: 1.5.278
+ node-releases: 2.0.27
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001766: {}
+
+ chai@6.2.2: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ compare-versions@6.1.1: {}
+
+ concat-map@0.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ cookie@1.1.1: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.2.3: {}
+
+ data-view-buffer@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-offset@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
+ detect-libc@2.1.2: {}
+
+ doctrine@2.1.0:
+ dependencies:
+ esutils: 2.0.3
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ electron-to-chromium@1.5.278: {}
+
+ enhanced-resolve@5.18.4:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
+ entities@4.5.0: {}
+
+ es-abstract@1.24.1:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.0
+ function.prototype.name: 1.1.8
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.1
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.3
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.10
+ string.prototype.trimend: 1.0.9
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.4
+ typed-array-length: 1.0.7
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.20
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-iterator-helpers@1.2.2:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-set-tostringtag: 2.1.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ iterator.prototype: 1.1.5
+ safe-array-concat: 1.1.3
+
+ es-module-lexer@1.7.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ es-shim-unscopables@1.1.0:
+ dependencies:
+ hasown: 2.0.2
+
+ es-to-primitive@1.3.0:
+ dependencies:
+ is-callable: 1.2.7
+ is-date-object: 1.1.0
+ is-symbol: 1.1.1
+
+ esbuild@0.27.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.2
+ '@esbuild/android-arm': 0.27.2
+ '@esbuild/android-arm64': 0.27.2
+ '@esbuild/android-x64': 0.27.2
+ '@esbuild/darwin-arm64': 0.27.2
+ '@esbuild/darwin-x64': 0.27.2
+ '@esbuild/freebsd-arm64': 0.27.2
+ '@esbuild/freebsd-x64': 0.27.2
+ '@esbuild/linux-arm': 0.27.2
+ '@esbuild/linux-arm64': 0.27.2
+ '@esbuild/linux-ia32': 0.27.2
+ '@esbuild/linux-loong64': 0.27.2
+ '@esbuild/linux-mips64el': 0.27.2
+ '@esbuild/linux-ppc64': 0.27.2
+ '@esbuild/linux-riscv64': 0.27.2
+ '@esbuild/linux-s390x': 0.27.2
+ '@esbuild/linux-x64': 0.27.2
+ '@esbuild/netbsd-arm64': 0.27.2
+ '@esbuild/netbsd-x64': 0.27.2
+ '@esbuild/openbsd-arm64': 0.27.2
+ '@esbuild/openbsd-x64': 0.27.2
+ '@esbuild/openharmony-arm64': 0.27.2
+ '@esbuild/sunos-x64': 0.27.2
+ '@esbuild/win32-arm64': 0.27.2
+ '@esbuild/win32-ia32': 0.27.2
+ '@esbuild/win32-x64': 0.27.2
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)):
+ dependencies:
+ eslint: 9.39.2(jiti@2.6.1)
+
+ eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1):
+ dependencies:
+ eslint: 9.39.2(jiti@2.6.1)
+ prettier: 3.8.1
+ prettier-linter-helpers: 1.0.1
+ synckit: 0.11.12
+ optionalDependencies:
+ eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
+
+ eslint-plugin-react-dom@2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@eslint-react/ast': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/core': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/eff': 2.7.4
+ '@eslint-react/shared': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/var': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ compare-versions: 6.1.1
+ eslint: 9.39.2(jiti@2.6.1)
+ string-ts: 2.3.1
+ ts-pattern: 5.9.0
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)):
+ dependencies:
+ '@babel/core': 7.28.6
+ '@babel/parser': 7.28.6
+ eslint: 9.39.2(jiti@2.6.1)
+ hermes-parser: 0.25.1
+ zod: 4.3.6
+ zod-validation-error: 4.0.2(zod@4.3.6)
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react-refresh@0.4.26(eslint@9.39.2(jiti@2.6.1)):
+ dependencies:
+ eslint: 9.39.2(jiti@2.6.1)
+
+ eslint-plugin-react-x@2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@eslint-react/ast': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/core': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/eff': 2.7.4
+ '@eslint-react/shared': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@eslint-react/var': 2.7.4(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.53.1
+ '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/types': 8.53.1
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ compare-versions: 6.1.1
+ eslint: 9.39.2(jiti@2.6.1)
+ is-immutable-type: 5.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ string-ts: 2.3.1
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-pattern: 5.9.0
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)):
+ dependencies:
+ array-includes: 3.1.9
+ array.prototype.findlast: 1.2.5
+ array.prototype.flatmap: 1.3.3
+ array.prototype.tosorted: 1.1.4
+ doctrine: 2.1.0
+ es-iterator-helpers: 1.2.2
+ eslint: 9.39.2(jiti@2.6.1)
+ estraverse: 5.3.0
+ hasown: 2.0.2
+ jsx-ast-utils: 3.3.5
+ minimatch: 3.1.2
+ object.entries: 1.1.9
+ object.fromentries: 2.0.8
+ object.values: 1.2.1
+ prop-types: 15.8.1
+ resolve: 2.0.0-next.5
+ semver: 6.3.1
+ string.prototype.matchall: 4.0.12
+ string.prototype.repeat: 1.0.0
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint@9.39.2(jiti@2.6.1):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.21.1
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
+ '@eslint/eslintrc': 3.3.3
+ '@eslint/js': 9.39.2
+ '@eslint/plugin-kit': 0.4.1
+ '@humanfs/node': 0.16.7
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.6.1
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ esutils@2.0.3: {}
+
+ expect-type@1.3.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-diff@1.3.0: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ flatted@3.3.3: {}
+
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ function.prototype.name@1.1.8:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ functions-have-names: 1.2.3
+ hasown: 2.0.2
+ is-callable: 1.2.7
+
+ functions-have-names@1.2.3: {}
+
+ generator-function@2.0.1: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-symbol-description@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@17.1.0: {}
+
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ happy-dom@20.3.7:
+ dependencies:
+ '@types/node': 25.0.10
+ '@types/whatwg-mimetype': 3.0.2
+ '@types/ws': 8.18.1
+ entities: 4.5.0
+ whatwg-mimetype: 3.0.0
+ ws: 8.19.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ has-bigints@1.1.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-proto@1.2.0:
+ dependencies:
+ dunder-proto: 1.0.1
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hermes-estree@0.25.1: {}
+
+ hermes-parser@0.25.1:
+ dependencies:
+ hermes-estree: 0.25.1
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.2
+ side-channel: 1.1.0
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-async-function@2.1.1:
+ dependencies:
+ async-function: 1.0.0
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-callable@1.2.7: {}
+
+ is-core-module@2.16.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-data-view@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ is-typed-array: 1.1.15
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-extglob@2.1.1: {}
+
+ is-finalizationregistry@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-generator-function@1.1.2:
+ dependencies:
+ call-bound: 1.0.4
+ generator-function: 2.0.1
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-immutable-type@5.0.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ ts-api-utils: 2.4.0(typescript@5.9.3)
+ ts-declaration-location: 1.0.7(typescript@5.9.3)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ is-map@2.0.3: {}
+
+ is-negative-zero@2.0.3: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.20
+
+ is-weakmap@2.0.2: {}
+
+ is-weakref@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ isarray@2.0.5: {}
+
+ isexe@2.0.0: {}
+
+ iterator.prototype@1.1.5:
+ dependencies:
+ define-data-property: 1.1.4
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ has-symbols: 1.1.0
+ set-function-name: 2.0.2
+
+ jiti@2.6.1: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ jsx-ast-utils@3.3.5:
+ dependencies:
+ array-includes: 3.1.9
+ array.prototype.flat: 1.3.3
+ object.assign: 4.1.7
+ object.values: 1.2.1
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.30.2:
+ optional: true
+
+ lightningcss-darwin-arm64@1.30.2:
+ optional: true
+
+ lightningcss-darwin-x64@1.30.2:
+ optional: true
+
+ lightningcss-freebsd-x64@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.30.2:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.30.2:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.30.2:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.30.2:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.30.2:
+ optional: true
+
+ lightningcss@1.30.2:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.30.2
+ lightningcss-darwin-arm64: 1.30.2
+ lightningcss-darwin-x64: 1.30.2
+ lightningcss-freebsd-x64: 1.30.2
+ lightningcss-linux-arm-gnueabihf: 1.30.2
+ lightningcss-linux-arm64-gnu: 1.30.2
+ lightningcss-linux-arm64-musl: 1.30.2
+ lightningcss-linux-x64-gnu: 1.30.2
+ lightningcss-linux-x64-musl: 1.30.2
+ lightningcss-win32-arm64-msvc: 1.30.2
+ lightningcss-win32-x64-msvc: 1.30.2
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash-es@4.17.23: {}
+
+ lodash.merge@4.6.2: {}
+
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ math-intrinsics@1.1.0: {}
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.27: {}
+
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
+ object.entries@1.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ object.fromentries@2.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-object-atoms: 1.1.1
+
+ object.values@1.2.1:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ obug@2.1.1: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ own-keys@1.0.1:
+ dependencies:
+ get-intrinsic: 1.3.0
+ object-keys: 1.1.1
+ safe-push-apply: 1.0.0
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-parse@1.0.7: {}
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.3: {}
+
+ possible-typed-array-names@1.1.0: {}
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ prettier-linter-helpers@1.0.1:
+ dependencies:
+ fast-diff: 1.3.0
+
+ prettier-plugin-tailwindcss@0.7.2(prettier@3.8.1):
+ dependencies:
+ prettier: 3.8.1
+
+ prettier@3.8.1: {}
+
+ prismjs@1.30.0: {}
+
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.3(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ scheduler: 0.27.0
+
+ react-hook-form@7.71.1(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+
+ react-icons@5.5.0(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+
+ react-is@16.13.1: {}
+
+ react-refresh@0.18.0: {}
+
+ react-router-dom@7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-router: 7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+
+ react-router@7.13.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ cookie: 1.1.1
+ react: 19.2.3
+ set-cookie-parser: 2.7.2
+ optionalDependencies:
+ react-dom: 19.2.3(react@19.2.3)
+
+ react@19.2.3: {}
+
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ which-builtin-type: 1.2.1
+
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
+ resolve-from@4.0.0: {}
+
+ resolve@2.0.0-next.5:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ rollup@4.56.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.56.0
+ '@rollup/rollup-android-arm64': 4.56.0
+ '@rollup/rollup-darwin-arm64': 4.56.0
+ '@rollup/rollup-darwin-x64': 4.56.0
+ '@rollup/rollup-freebsd-arm64': 4.56.0
+ '@rollup/rollup-freebsd-x64': 4.56.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.56.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.56.0
+ '@rollup/rollup-linux-arm64-gnu': 4.56.0
+ '@rollup/rollup-linux-arm64-musl': 4.56.0
+ '@rollup/rollup-linux-loong64-gnu': 4.56.0
+ '@rollup/rollup-linux-loong64-musl': 4.56.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.56.0
+ '@rollup/rollup-linux-ppc64-musl': 4.56.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.56.0
+ '@rollup/rollup-linux-riscv64-musl': 4.56.0
+ '@rollup/rollup-linux-s390x-gnu': 4.56.0
+ '@rollup/rollup-linux-x64-gnu': 4.56.0
+ '@rollup/rollup-linux-x64-musl': 4.56.0
+ '@rollup/rollup-openbsd-x64': 4.56.0
+ '@rollup/rollup-openharmony-arm64': 4.56.0
+ '@rollup/rollup-win32-arm64-msvc': 4.56.0
+ '@rollup/rollup-win32-ia32-msvc': 4.56.0
+ '@rollup/rollup-win32-x64-gnu': 4.56.0
+ '@rollup/rollup-win32-x64-msvc': 4.56.0
+ fsevents: 2.3.3
+
+ safe-array-concat@1.1.3:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ isarray: 2.0.5
+
+ safe-push-apply@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ isarray: 2.0.5
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.3: {}
+
+ set-cookie-parser@2.7.2: {}
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ stackback@0.0.2: {}
+
+ std-env@3.10.0: {}
+
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
+ string-ts@2.3.1: {}
+
+ string.prototype.matchall@4.0.12:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ regexp.prototype.flags: 1.5.4
+ set-function-name: 2.0.2
+ side-channel: 1.1.0
+
+ string.prototype.repeat@1.0.0:
+ dependencies:
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+
+ string.prototype.trim@1.2.10:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.1
+ es-object-atoms: 1.1.1
+ has-property-descriptors: 1.0.2
+
+ string.prototype.trimend@1.0.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ strip-json-comments@3.1.1: {}
+
+ sugarss@5.0.1(postcss@8.5.6):
+ dependencies:
+ postcss: 8.5.6
+ optional: true
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ synckit@0.11.12:
+ dependencies:
+ '@pkgr/core': 0.2.9
+
+ tailwindcss@4.1.18: {}
+
+ tapable@2.3.0: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.0.2: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinyrainbow@3.0.3: {}
+
+ ts-api-utils@2.4.0(typescript@5.9.3):
+ dependencies:
+ typescript: 5.9.3
+
+ ts-declaration-location@1.0.7(typescript@5.9.3):
+ dependencies:
+ picomatch: 4.0.3
+ typescript: 5.9.3
+
+ ts-pattern@5.9.0: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typed-array-buffer@1.0.3:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-length@1.0.3:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-offset@1.0.4:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+ reflect.getprototypeof: 1.0.10
+
+ typed-array-length@1.0.7:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ possible-typed-array-names: 1.1.0
+ reflect.getprototypeof: 1.0.10
+
+ typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.9.3: {}
+
+ unbox-primitive@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-bigints: 1.1.0
+ has-symbols: 1.1.0
+ which-boxed-primitive: 1.1.1
+
+ undici-types@7.16.0: {}
+
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
+ dependencies:
+ browserslist: 4.28.1
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-context-selector@2.0.0(react@19.2.3)(scheduler@0.27.0):
+ dependencies:
+ react: 19.2.3
+ scheduler: 0.27.0
+
+ vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)):
+ dependencies:
+ esbuild: 0.27.2
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.56.0
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 25.0.10
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ lightningcss: 1.30.2
+ sugarss: 5.0.1(postcss@8.5.6)
+
+ vitest@4.0.18(@types/node@25.0.10)(happy-dom@20.3.7)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)):
+ dependencies:
+ '@vitest/expect': 4.0.18
+ '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6)))
+ '@vitest/pretty-format': 4.0.18
+ '@vitest/runner': 4.0.18
+ '@vitest/snapshot': 4.0.18
+ '@vitest/spy': 4.0.18
+ '@vitest/utils': 4.0.18
+ es-module-lexer: 1.7.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.10.0
+ tinybench: 2.9.0
+ tinyexec: 1.0.2
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.0.3
+ vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(sugarss@5.0.1(postcss@8.5.6))
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 25.0.10
+ happy-dom: 20.3.7
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - yaml
+
+ whatwg-mimetype@3.0.0: {}
+
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-builtin-type@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ function.prototype.name: 1.1.8
+ has-tostringtag: 1.0.2
+ is-async-function: 2.1.1
+ is-date-object: 1.1.0
+ is-finalizationregistry: 1.1.1
+ is-generator-function: 1.1.2
+ is-regex: 1.2.1
+ is-weakref: 1.1.1
+ isarray: 2.0.5
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.20
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.20:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ word-wrap@1.2.5: {}
+
+ ws@8.19.0: {}
+
+ yallist@3.1.1: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod-validation-error@4.0.2(zod@4.3.6):
+ dependencies:
+ zod: 4.3.6
+
+ zod@4.3.6: {}
diff --git a/frontend/src/App/error-boundary.tsx b/frontend/src/App/error-boundary.tsx
new file mode 100644
index 0000000..3fec8cd
--- /dev/null
+++ b/frontend/src/App/error-boundary.tsx
@@ -0,0 +1,42 @@
+import type { ErrorInfo, PropsWithChildren, ReactNode } from 'react';
+import { PureComponent } from 'react';
+
+export class ErrorBoundary extends PureComponent {
+ state: Readonly<{ error: unknown }>;
+ constructor(props: PropsWithChildren) {
+ super(props);
+ this.state = { error: undefined };
+ }
+
+ componentDidCatch(exception: Error, errorInfo: ErrorInfo): void {
+ const meta = {
+ location: window.location.href,
+ time: new Date().toISOString(),
+ error: {
+ exception: exception.stack ?? exception.message,
+ ...errorInfo,
+ },
+ };
+ this.setState({ error: meta });
+ }
+
+ render(): ReactNode {
+ if (!this.state.error) return this.props.children;
+ return (
+
+
You encountered a bug
+
Error details:
+
+ );
+ }
+}
diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx
new file mode 100755
index 0000000..88b9186
--- /dev/null
+++ b/frontend/src/App/index.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from 'react';
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import { ErrorBoundary } from './error-boundary';
+import { Search } from './pages/search';
+import { File } from './pages/file';
+import { Header } from './layout/header';
+import { SearchContextProvider } from './pages/store';
+import { KeyboardShortcuts } from './pages/keyboard/keyboard-shortcuts';
+import { Footer } from './layout/footer';
+
+export default function App(): ReactNode {
+ return (
+
+
+
+
+
+
+ } />
+ } />
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/App/layout/footer.tsx b/frontend/src/App/layout/footer.tsx
new file mode 100644
index 0000000..b394e1b
--- /dev/null
+++ b/frontend/src/App/layout/footer.tsx
@@ -0,0 +1,49 @@
+import { useSearchContext } from '../pages/store';
+import type { ReactNode } from 'react';
+import { backendUrl } from '../libs/fetcher';
+
+function formatDate(date: Date): string {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+}
+
+export function Footer(): ReactNode {
+ const result = useSearchContext((ctx) => ctx.fileResult?.result ?? ctx.searchResult?.result);
+
+ if (!result) return null;
+
+ const text =
+ 'files' in result
+ ? `${result.hits} matches${result.truncated ? ' (truncated)' : ''} in ${result.files.length} files${result.matchedFiles > result.files.length ? ` (${result.matchedFiles} matched)` : ''}`
+ : result.matches.length > 0
+ ? `${result.matches.length} matches`
+ : '';
+ return (
+
+ );
+}
diff --git a/frontend/src/App/layout/header.tsx b/frontend/src/App/layout/header.tsx
new file mode 100644
index 0000000..9508178
--- /dev/null
+++ b/frontend/src/App/layout/header.tsx
@@ -0,0 +1,111 @@
+import type { CSSProperties, ReactNode } from 'react';
+import type { Filters } from '../pages/store';
+import { useSearchContext } from '../pages/store';
+import { Controller } from 'react-hook-form';
+import type { Control } from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
+import { createUrlParams } from '../pages/store/url-params';
+import { LuArrowDown, LuArrowUp } from 'react-icons/lu';
+import { unfocus } from '../pages/keyboard/use-keyboard-shortcuts';
+
+function TextInput({
+ name,
+ control,
+ width,
+ ta,
+ ...props
+}: {
+ name: keyof Omit;
+ control: Control;
+ ta?: CSSProperties['textAlign'];
+ width: number;
+ placeholder?: string;
+}): ReactNode {
+ return (
+ (
+
+ )}
+ {...{ name, control }}
+ />
+ );
+}
+
+function ToggleButton({
+ control,
+ name,
+ children,
+}: {
+ control: Control;
+ name: 'caseInsensitive';
+ children: ReactNode;
+}): ReactNode {
+ return (
+ (
+
+ )}
+ />
+ );
+}
+
+export function Header(): ReactNode {
+ const form = useSearchContext((ctx) => ctx.form);
+ const navigate = useNavigate();
+
+ return (
+
+ );
+}
diff --git a/frontend/src/App/libs/fetcher.ts b/frontend/src/App/libs/fetcher.ts
new file mode 100644
index 0000000..650f087
--- /dev/null
+++ b/frontend/src/App/libs/fetcher.ts
@@ -0,0 +1,129 @@
+import { useLayoutEffect, useReducer, useRef } from 'react';
+import { useCustomCompareCallback } from './use-custom-compare-callback';
+
+enum Method {
+ GET = 'GET',
+}
+
+export type Params = {
+ method?: Method;
+ body?: string | FormData;
+ json?: object;
+ headers?: Record;
+};
+
+export type HookParams = Params & {
+ responseMapper?: (response: T) => U;
+};
+
+export type HttpState =
+ | { loading: true; error?: undefined; response?: undefined }
+ | { loading?: false; error: HttpError; response?: undefined }
+ | { loading?: false; error?: undefined; response: T };
+export type HttpStateWithReload = HttpState & { reloading: boolean };
+export type HttpStateWithRefresh = HttpStateWithReload & {
+ refresh: () => Promise;
+};
+
+export class HttpError extends Error {
+ code?: number;
+ constructor(message: string, code?: number) {
+ super(message);
+ this.code = code;
+ }
+}
+
+export function Get(url: string, params?: Params): Promise {
+ return Fetch(Method.GET, url, params);
+}
+
+function reducer(state: HttpStateWithReload, action: Partial>): HttpStateWithReload {
+ const newState = { ...state, ...action };
+ if (action.response) delete newState.error;
+ else if (action.error) delete newState.response;
+ return newState as HttpStateWithReload;
+}
+
+export function useGet(url: string, params: HookParams = {}): HttpStateWithRefresh {
+ const initialState: HttpStateWithReload = {
+ loading: true,
+ reloading: true,
+ };
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const cancelled = useRef(false);
+
+ const refresh = useCustomCompareCallback(() => {
+ if (cancelled.current) return Promise.reject(new Error('Cancelled'));
+ dispatch({ reloading: true });
+ return Get(url, params)
+ .then((originalResponse) => {
+ const response = params.responseMapper
+ ? params.responseMapper(originalResponse)
+ : (originalResponse as unknown as U);
+ if (!cancelled.current) dispatch({ loading: false, reloading: false, response });
+ return response;
+ })
+ .catch((err: unknown) => {
+ const error = err instanceof Error ? err : new Error(String(err));
+ if (!cancelled.current) dispatch({ loading: false, reloading: false, error });
+ const newError = new HttpError(`GET ${url}: ${error.message}`);
+ newError.stack = error.stack;
+ return Promise.reject(newError);
+ });
+ }, [url, params]);
+
+ useLayoutEffect(() => {
+ cancelled.current = false;
+ return (): void => {
+ cancelled.current = true;
+ };
+ }, []);
+ useLayoutEffect(() => {
+ // Initial parameters have changed, set loading
+ dispatch({ loading: true });
+ void refresh();
+ }, [refresh]);
+
+ return { ...state, refresh };
+}
+
+export function backendUrl(): string {
+ return window.localStorage.getItem('code-search-backend') ?? 'http://localhost:8080';
+}
+
+async function Fetch(method: Method, url: string, params: Params = {}): Promise {
+ if (!/^[A-Za-z]+:\/\//.exec(url)) url = backendUrl() + url;
+
+ params = { ...params }; // Copy to avoid mutating the original
+ params.method = method;
+
+ return (
+ fetch(url, params)
+ // Reject promise if response is not OK
+ .then((response) => {
+ if (response.ok) return response;
+ return response.text().then((text) => {
+ let message = text;
+ try {
+ const json = JSON.parse(text) as object | { message: string };
+ if ('message' in json) message = json.message;
+ } catch {
+ // not JSON
+ }
+ return Promise.reject(new HttpError(message, response.status));
+ });
+ })
+
+ // automatically return the data if it's a known content type
+ .then((response) => {
+ const contentType = response.headers.get('content-type');
+ if (!contentType) return response;
+ if (contentType.includes('application/json')) {
+ return response.json();
+ } else if (contentType.includes('text/plain')) {
+ return response.text();
+ }
+ return response;
+ }) as Promise
+ );
+}
diff --git a/frontend/src/App/libs/use-custom-compare-callback.ts b/frontend/src/App/libs/use-custom-compare-callback.ts
new file mode 100644
index 0000000..281c4c2
--- /dev/null
+++ b/frontend/src/App/libs/use-custom-compare-callback.ts
@@ -0,0 +1,27 @@
+import { type DependencyList, useCallback, useEffect, useState } from 'react';
+import { isEqual } from 'lodash-es';
+
+export function useCustomCompareMemoize(
+ deps: DependencyList,
+ depsEqual: (a: DependencyList, b: DependencyList) => boolean = isEqual,
+): DependencyList {
+ const [currentValue, setCurrentValue] = useState(deps);
+ if (!depsEqual(deps, currentValue)) setCurrentValue(deps);
+ return currentValue;
+}
+
+export function useCustomCompareCallback(
+ callback: (...args: DependencyList) => T,
+ deps: DependencyList,
+ depsEqual?: (a: DependencyList, b: DependencyList) => boolean,
+): (...args: DependencyList) => T {
+ return useCallback(callback, useCustomCompareMemoize(deps, depsEqual));
+}
+
+export function useCustomCompareEffect(
+ effect: () => void,
+ deps: DependencyList,
+ depsEqual?: (a: DependencyList, b: DependencyList) => boolean,
+): void {
+ useEffect(effect, useCustomCompareMemoize(deps, depsEqual));
+}
diff --git a/frontend/src/App/pages/file/code-highlight.tsx b/frontend/src/App/pages/file/code-highlight.tsx
new file mode 100644
index 0000000..88917d5
--- /dev/null
+++ b/frontend/src/App/pages/file/code-highlight.tsx
@@ -0,0 +1,149 @@
+import { type ReactNode, useState } from 'react';
+import { useCustomCompareEffect } from '../../libs/use-custom-compare-callback';
+import Prism from 'prismjs';
+import type { Range } from '../store';
+
+// Import Prism languages
+import 'prismjs/components/prism-bash';
+import 'prismjs/components/prism-c';
+import 'prismjs/components/prism-cmake';
+import 'prismjs/components/prism-cpp';
+import 'prismjs/components/prism-css';
+import 'prismjs/components/prism-docker';
+import 'prismjs/components/prism-go';
+import 'prismjs/components/prism-hcl';
+import 'prismjs/components/prism-java';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/components/prism-typescript';
+import 'prismjs/components/prism-json';
+import 'prismjs/components/prism-jsx';
+import 'prismjs/components/prism-makefile';
+import 'prismjs/components/prism-markdown';
+import 'prismjs/components/prism-markup';
+import 'prismjs/components/prism-python';
+import 'prismjs/components/prism-ruby';
+import 'prismjs/components/prism-tsx';
+import 'prismjs/components/prism-typescript';
+import 'prismjs/components/prism-yaml';
+
+const fileExtensionToLanguage = Object.fromEntries(
+ Object.entries({
+ bash: ['sh', 'bash'],
+ c: ['c'],
+ cpp: ['cpp', 'cppm', 'hpp', 'h'],
+ cmake: ['cmake'],
+ css: ['css'],
+ docker: ['dockerfile'],
+ go: ['go'],
+ hcl: ['hcl', 'tf'],
+ java: ['java'],
+ javascript: ['jsx'],
+ json: ['json', 'jsonc', 'jsonl'],
+ jsx: ['jsx'],
+ makefile: ['makefile'],
+ markdown: ['md', 'mdx'],
+ markup: ['html', 'svg', 'xml'],
+ python: ['py'],
+ ruby: ['rb'],
+ tsx: ['tsx'],
+ typescript: ['ts'],
+ yaml: ['yaml', 'yml'],
+ }).flatMap(([language, extensions]) => extensions.map((ext) => [ext, language])),
+);
+
+function pathToLanguage(extension: string): string {
+ const fileIndex = extension.lastIndexOf('/');
+ const extensionIndex = extension.lastIndexOf('.');
+ const ext = extension.substring(Math.max(fileIndex, extensionIndex) + 1).toLocaleLowerCase();
+ return fileExtensionToLanguage[ext] ?? 'text';
+}
+
+type LineMatch = { line: number; range: Range };
+function getHighlightsForLine(lineIdx: number, ranges: LineMatch[]): [number, number][] {
+ return ranges.filter((r) => r.line - 1 === lineIdx).map((r) => r.range);
+}
+
+function renderPrismTokens(tokens: Prism.TokenStream, highlights: [number, number][]): ReactNode[] {
+ let charIdx = 0;
+ let highlightIdx = 0;
+ let openHighlight = false;
+
+ function walk(token: Prism.TokenStream): ReactNode[] {
+ const nodes: ReactNode[] = [];
+ for (const part of Array.isArray(token) ? token : [token]) {
+ if (typeof part === 'string') {
+ for (let i = 0; i < part.length; ) {
+ if (charIdx === highlights[highlightIdx]?.[0]) {
+ openHighlight = true;
+ } else if (charIdx === highlights[highlightIdx]?.[1]) {
+ openHighlight = false;
+ highlightIdx++;
+ }
+ const nextHighlightStart = highlights[highlightIdx]?.[0] ?? part.length + charIdx;
+ const nextHighlightEnd = highlights[highlightIdx]?.[1] ?? part.length + charIdx;
+ const chunkEnd = openHighlight
+ ? Math.min(nextHighlightEnd - charIdx + i, part.length)
+ : Math.min(nextHighlightStart - charIdx + i, part.length);
+ const chunk = part.slice(i, chunkEnd);
+ if (openHighlight) {
+ nodes.push(
+
+ {chunk}
+ ,
+ );
+ } else {
+ nodes.push(chunk);
+ }
+ i += chunk.length;
+ charIdx += chunk.length;
+ }
+ } else if (typeof part === 'object' && part !== null) {
+ nodes.push(
+
+ {walk(part.content)}
+ ,
+ );
+ }
+ }
+ return nodes;
+ }
+ return walk(tokens);
+}
+
+function highlightCodeReact(code: string, language: string, ranges: LineMatch[]): ReactNode {
+ const lines = code.split('\n');
+ const prismLang = Prism.languages[language];
+ return (
+
+
+ {lines.map((line, idx) => {
+ const highlights = getHighlightsForLine(idx, ranges);
+ const tokens = Prism.tokenize(line, prismLang);
+ return (
+
+ {renderPrismTokens(tokens, highlights)}
+
+ );
+ })}
+
+
+ );
+}
+
+export function CodeHighlight({
+ code,
+ path,
+ ranges = [],
+}: {
+ code: string;
+ path: string;
+ ranges?: LineMatch[];
+}): ReactNode {
+ const [content, setContent] = useState({code});
+ useCustomCompareEffect(() => {
+ const language = pathToLanguage(path);
+ setContent(highlightCodeReact(code, language, ranges));
+ }, [code, path, ranges]);
+
+ return content;
+}
diff --git a/frontend/src/App/pages/file/index.tsx b/frontend/src/App/pages/file/index.tsx
new file mode 100644
index 0000000..5c9c189
--- /dev/null
+++ b/frontend/src/App/pages/file/index.tsx
@@ -0,0 +1,91 @@
+import type { ReactNode } from 'react';
+import { useEffect } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import { CodeHighlight } from './code-highlight';
+import type { LineMatch } from '../store';
+import { useSearchContext } from '../store';
+
+function countLines(str: string): number {
+ let count = 1;
+ for (const char of str) {
+ if (char === '\n') count++;
+ }
+ return count;
+}
+
+function FileContent({ code, path, ranges }: { code: string; path: string; ranges: LineMatch[] }): ReactNode {
+ const { hash } = useLocation();
+ useEffect(() => {
+ if (hash.length === 0) return;
+ const fragment = hash.substring(1);
+
+ void (async function (): Promise {
+ for (let i = 0; i < 3; i++) {
+ const element = document.getElementById(fragment);
+ if (element) {
+ for (const elem of document.getElementsByClassName('line highlight')) elem.classList.remove('highlight');
+ element.classList.add('highlight');
+ element.scrollIntoView({ block: 'center' });
+ break;
+ }
+ await new Promise((resolve) => setTimeout(resolve, 100)); // thisisfine.jpg
+ }
+ })();
+ }, [hash]);
+
+ return (
+
+
+ {Array.from({ length: countLines(code) })
+ .map((_, i) => i + 1)
+ .map((i) => (
+
+ {i}
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export function File(): ReactNode {
+ const resultState = useSearchContext((ctx) => ctx.fileResult);
+ if (resultState == null) return null;
+
+ const { loading, error, result } = resultState;
+ if (loading) return Loading...
;
+ if (error)
+ return (
+
+ Error: {error.message}
+
+ );
+
+ const parts = `${result!.directory}/${result!.path}`.split('/');
+ return (
+
+
+ {parts.map((name, i, arr) => (
+
+ {i > 0 ? / : null}
+ {i === arr.length - 1 ? (
+ {name}
+ ) : (
+
+ {name}
+
+ )}
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/App/pages/keyboard/__test__/sequence-key-listener.test.ts b/frontend/src/App/pages/keyboard/__test__/sequence-key-listener.test.ts
new file mode 100644
index 0000000..97c5ebb
--- /dev/null
+++ b/frontend/src/App/pages/keyboard/__test__/sequence-key-listener.test.ts
@@ -0,0 +1,82 @@
+import { expect, test, vi } from 'vitest';
+import { listener } from '../sequence-key-listener';
+
+test('bind and unbind', () => {
+ const func1 = (): number => 0;
+ const func2 = (): number => 0;
+ const func3 = (): number => 0;
+
+ listener.bind('abc', func1);
+ listener.bind('d', func2);
+ listener.bind(['a', 'b', 'd'], func3);
+ expect(listener.root).toEqual({
+ a: { b: { c: { callback: func1 }, d: { callback: func3 } } },
+ d: { callback: func2 },
+ });
+
+ listener.unbind('abc');
+ expect(listener.root).toEqual({
+ a: { b: { d: { callback: func3 } } },
+ d: { callback: func2 },
+ });
+
+ listener.unbind('abd');
+ expect(listener.root).toEqual({ d: { callback: func2 } });
+
+ listener.unbind('d');
+ expect(listener.root).toEqual({});
+});
+
+test('invalid binds and unbinds', () => {
+ const func = (): number => 0;
+ listener.bind('abc', func);
+ expect(() => {
+ listener.bind('ab', func);
+ }).toThrow('Other sequence starting with a,b already bound');
+
+ expect(() => {
+ listener.bind('abcd', func);
+ }).toThrow('Cannot bind sequence a,b,c,d: a,b already bound');
+
+ expect(() => {
+ listener.unbind('ab');
+ }).toThrow('Cannot unbind missing sequence a,b');
+ listener.unbind('abc');
+});
+
+test('sequence callbacks called', () => {
+ const origNow = Date.now;
+
+ const fn1 = vi.fn();
+ const fn2 = vi.fn();
+ listener.bind('abc', fn1);
+ listener.bind('d', fn2);
+ const triggerKeyAndAssertFnCalls = (key: string, ctrlKey: boolean, fn1count: number, fn2count: number): void => {
+ listener.keyDownHandler(new KeyboardEvent('keydown', { key, ctrlKey }));
+ expect(fn1).toHaveBeenCalledTimes(fn1count);
+ expect(fn2).toHaveBeenCalledTimes(fn2count);
+ };
+
+ Date.now = (): number => 123456000;
+ triggerKeyAndAssertFnCalls('d', true, 0, 0);
+ triggerKeyAndAssertFnCalls('d', false, 0, 1);
+ triggerKeyAndAssertFnCalls('d', false, 0, 2);
+
+ triggerKeyAndAssertFnCalls('a', false, 0, 2);
+ triggerKeyAndAssertFnCalls('b', false, 0, 2);
+ triggerKeyAndAssertFnCalls('c', false, 1, 2); // this completes abc sequence
+ triggerKeyAndAssertFnCalls('c', false, 1, 2); // c again should not re-trigger
+
+ triggerKeyAndAssertFnCalls('a', false, 1, 2);
+ triggerKeyAndAssertFnCalls('b', true, 1, 2);
+ triggerKeyAndAssertFnCalls('c', false, 1, 2); // b was pressed with modifier, doesn't count
+
+ triggerKeyAndAssertFnCalls('a', false, 1, 2);
+ triggerKeyAndAssertFnCalls('b', false, 1, 2);
+ Date.now = (): number => 123458000;
+ triggerKeyAndAssertFnCalls('c', false, 1, 2); // 2 sec between b and c, doesn't count
+
+ listener.unbind('abc');
+ listener.unbind('d');
+ Date.now = origNow;
+});
diff --git a/frontend/src/App/pages/keyboard/keyboard-shortcuts.tsx b/frontend/src/App/pages/keyboard/keyboard-shortcuts.tsx
new file mode 100644
index 0000000..41f842a
--- /dev/null
+++ b/frontend/src/App/pages/keyboard/keyboard-shortcuts.tsx
@@ -0,0 +1,105 @@
+import type { ReactNode } from 'react';
+import { Fragment } from 'react';
+import { useKeyboardShortcuts } from './use-keyboard-shortcuts';
+
+const columns: {
+ header?: string;
+ keys: { keys: string[]; description: string; joiner?: string }[];
+}[] = [
+ {
+ header: 'General',
+ keys: [
+ { keys: ['Esc'], description: 'Unfocus filter input' },
+ { keys: ['?'], description: 'Toggle help (this window)' },
+ { keys: ['q'], description: 'Focus line filter input' },
+ { keys: ['f'], description: 'Focus file filter input' },
+ { keys: ['x'], description: 'Focus exclude path input' },
+ { keys: ['['], description: 'Focus context before input' },
+ { keys: [']'], description: 'Focus context after input' },
+ { keys: ['i'], description: 'Toggle case sensitivity' },
+ { keys: ['s'], description: 'Search' },
+ { keys: ['r'], description: 'Reset search form' },
+ ],
+ },
+ {
+ header: 'Navigation',
+ keys: [
+ { keys: ['k', '▲'], description: 'Select hit above', joiner: ' or ' },
+ { keys: ['j', '▼'], description: 'Select hit below', joiner: ' or ' },
+ ],
+ },
+ {
+ header: 'Open selected file',
+ keys: [
+ { keys: ['o', 'O'], description: 'In file view (tab / window)' },
+ { keys: ['f', 'F'], description: 'In file in GitHub (tab / window)' },
+ { keys: ['b', 'B'], description: 'In blame in GitHub (tab / window)' },
+ { keys: ['h', 'H'], description: 'In history in GitHub (tab / window)' },
+ ],
+ },
+];
+
+function Column({
+ header,
+ keys,
+}: {
+ header?: string;
+ keys: { keys: string[]; description: string; joiner?: string }[];
+}): ReactNode {
+ return (
+
+ {header &&
{header}
}
+
+ {keys.map(({ keys, description, joiner = ' / ' }, i1: number) => (
+
+
+ {keys.map((key, i2: number) => (
+
+ {i2 > 0 && {joiner}}
+ {key}
+
+ ))}
+
+ {description}
+
+ ))}
+
+
+ );
+}
+
+export function KeyboardShortcuts(): ReactNode {
+ const [open, setOpen] = useKeyboardShortcuts();
+ if (!open) return null;
+
+ const onClose = (): void => {
+ setOpen(false);
+ };
+ return (
+
+
{
+ e.stopPropagation();
+ }}
+ >
+
Keyboard Shortcuts
+
+ {columns.map((col, i: number) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/App/pages/keyboard/sequence-key-listener.ts b/frontend/src/App/pages/keyboard/sequence-key-listener.ts
new file mode 100644
index 0000000..bae2a68
--- /dev/null
+++ b/frontend/src/App/pages/keyboard/sequence-key-listener.ts
@@ -0,0 +1,88 @@
+import { isEmpty } from 'lodash-es';
+
+type Callback = (event: KeyboardEvent) => void;
+type Node = { [key: string]: Node } & { callback?: Callback };
+
+class SequenceKeyListener {
+ root: Node;
+ current: Node;
+ lastKeyAt: number;
+ target: Document;
+ keyDownHandler: Callback;
+ constructor(target: Document) {
+ this.root = {};
+ this.current = this.root;
+ this.lastKeyAt = 0;
+ this.target = target;
+ this.keyDownHandler = this._keyDownHandler.bind(this);
+ }
+
+ bind(sequence: string[] | string, callback: Callback): void {
+ if (this.target && isEmpty(this.root)) this.target.addEventListener('keydown', this.keyDownHandler);
+
+ sequence = Array.isArray(sequence) ? sequence : sequence.split('');
+ let obj: Node = this.root;
+ for (let i = 0; i < sequence.length; i++) {
+ const key = sequence[i];
+ obj[key] ??= {};
+ obj = obj[key];
+
+ if (obj.callback)
+ throw new Error(`Cannot bind sequence ${String(sequence)}: ${String(sequence.slice(0, i))} already bound`);
+ }
+
+ if (Object.keys(obj).length > 0) throw new Error(`Other sequence starting with ${String(sequence)} already bound`);
+ obj.callback = callback;
+ }
+
+ unbind(sequence: string[] | string, obj?: Node, index = 0): void {
+ sequence = Array.isArray(sequence) ? sequence : sequence.split('');
+ obj ??= this.root;
+ if (index >= sequence.length) {
+ if (obj.callback) {
+ delete obj.callback;
+ return;
+ }
+ } else {
+ const c = sequence[index];
+ if (obj[c] != null) {
+ this.unbind(sequence, obj[c], index + 1);
+ if (Object.keys(obj[c]).length === 0) {
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete obj[c];
+ }
+ if (this.target && isEmpty(this.root)) this.target.removeEventListener('keydown', this.keyDownHandler);
+ return;
+ }
+ }
+
+ throw new Error('Cannot unbind missing sequence ' + String(sequence));
+ }
+
+ _keyDownHandler(event: KeyboardEvent): void {
+ const { key, target, altKey, ctrlKey, metaKey } = event;
+ if (
+ key !== 'Escape' &&
+ target instanceof HTMLInputElement &&
+ (target.getAttribute('type') === 'text' || target.getAttribute('type') == null)
+ )
+ return; // Ignore keys while typing in some 1000) this.current = this.root;
+
+ this.lastKeyAt = now;
+ this.current = this.current[key] || this.root[key] || this.root;
+ if (this.current.callback) {
+ event.preventDefault();
+ this.current.callback(event);
+ this.current = this.root;
+ }
+ }
+}
+
+export const listener = new SequenceKeyListener(window.document);
diff --git a/frontend/src/App/pages/keyboard/use-keyboard-shortcuts.ts b/frontend/src/App/pages/keyboard/use-keyboard-shortcuts.ts
new file mode 100644
index 0000000..d0661eb
--- /dev/null
+++ b/frontend/src/App/pages/keyboard/use-keyboard-shortcuts.ts
@@ -0,0 +1,104 @@
+import { useEffect, useState } from 'react';
+import type { NavigateFunction } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
+import { listener } from './sequence-key-listener';
+import type { Filters, SelectedHit } from '../store';
+import { ACTION, dispatch, useSearchContext } from '../store';
+import type { FieldPath, FieldPathValue, UseFormReturn } from 'react-hook-form';
+import { createUrlParams } from '../store/url-params';
+
+const testFileRegex = '/tests?/|_tests?\\b|Tests?[^a-z]|/systemtests/|\\.html$';
+
+const pathAnchor = (hit: SelectedHit): string => `${hit.path}${hit.line > 0 ? `#L${hit.line}` : ''}`;
+
+export function unfocus(): void {
+ const elem = document.activeElement;
+ if (elem instanceof HTMLElement) elem.blur();
+}
+
+function ghUrl(view: string): (hit: SelectedHit) => string {
+ return (hit) => `${hit.repository}/${view}/${hit.branch}/${pathAnchor(hit)}`;
+}
+
+export type UrlGenerator = (hit: SelectedHit) => string | undefined;
+export const URL_GENERATORS: readonly {
+ key: string;
+ name: string;
+ url: UrlGenerator;
+}[] = Object.freeze([
+ {
+ key: 'o',
+ name: 'file view',
+ url: (hit) => `/file/${hit.directory}/${pathAnchor(hit)}`,
+ },
+ { key: 'g', name: 'file view in GitHub', url: ghUrl('blob') },
+ { key: 'b', name: 'blame view', url: ghUrl('blame') },
+ { key: 'h', name: 'history view', url: ghUrl('commits') },
+]);
+
+const goToDispatcher = (navigate: NavigateFunction, urlGenerator: UrlGenerator, newWindow: boolean) => (): void => {
+ dispatch([
+ ACTION.CALLBACK_SELECTED_HIT,
+ (selectedHit: SelectedHit): void => {
+ const url = urlGenerator(selectedHit);
+ if (url == null) return;
+ if (newWindow) window.open(url);
+ else if (url.startsWith('http')) window.location.href = url;
+ else void navigate(url);
+ },
+ ]);
+};
+
+function modify>(
+ form: UseFormReturn,
+ field: TFieldName,
+ mapper: (current: FieldPathValue) => FieldPathValue,
+): () => void {
+ return () => {
+ form.setValue(field, mapper(form.getValues()[field] as FieldPathValue));
+ };
+}
+
+export function useKeyboardShortcuts(): [boolean, (open: boolean) => void] {
+ const navigate = useNavigate();
+ const [open, setOpen] = useState(false);
+ const form = useSearchContext((ctx) => ctx.form);
+
+ // p/n select prev/next file
+ // d/D exclude file selected directory/extension
+ // +/- expand/collapse file hits
+
+ // prettier-ignore
+ useEffect(() => {
+ const binds: [string | string[], (event: KeyboardEvent) => void][] = [
+ [['Escape'], unfocus],
+ ['q', (): void => { form.setFocus('query'); }],
+ ['f', (): void => { form.setFocus('file'); }],
+ ['x', (): void => { form.setFocus('excludeFile'); }],
+ ['i', modify(form, 'caseInsensitive', (cur) => !cur)],
+ ['[', (): void => { form.setFocus('numLinesBefore'); }],
+ [']', (): void => { form.setFocus('numLinesAfter'); }],
+ ['s', (): void => { void navigate(`/${createUrlParams(form.getValues())}`) }],
+ ['?', (): void => { setOpen((open) => !open); }],
+
+ ['r', (): void => { void navigate('/') }],
+ ['t', modify(form, 'excludeFile', (cur) => cur === testFileRegex ? '' : testFileRegex)],
+
+ ['k', (): void => { dispatch([ACTION.SELECT_PREVIOUS]); }],
+ [['ArrowUp'], (): void => { dispatch([ACTION.SELECT_PREVIOUS]); }],
+ ['j', (): void => { dispatch([ACTION.SELECT_NEXT]); }],
+ [['ArrowDown'], (): void => { dispatch([ACTION.SELECT_NEXT]); }],
+ ];
+
+ URL_GENERATORS.forEach(({ key, url }) => {
+ binds.push([key, goToDispatcher(navigate, url, false)]);
+ binds.push([key.toUpperCase(), goToDispatcher(navigate, url, true)]);
+ });
+
+ binds.forEach(([sequence, callback]) => { listener.bind(sequence, callback); });
+
+ return (): void => { binds.forEach(([sequence]) => { listener.unbind(sequence); }); };
+ }, [navigate, form]);
+
+ return [open, setOpen];
+}
diff --git a/frontend/src/App/pages/search/index.tsx b/frontend/src/App/pages/search/index.tsx
new file mode 100644
index 0000000..7acf28c
--- /dev/null
+++ b/frontend/src/App/pages/search/index.tsx
@@ -0,0 +1,76 @@
+import type { ReactNode } from 'react';
+import { Fragment } from 'react';
+import { Link } from 'react-router-dom';
+import { CodeHighlight } from '../file/code-highlight';
+import type { File, Line } from '../store';
+import { useSearchContext } from '../store';
+
+function CodeLine({ line, directory, path }: { line: Line; directory: string; path: string }): ReactNode {
+ const link = `/file/${directory}/${path}${window.location.search}#L${line.number}`;
+ const isSelected = useSearchContext(
+ (ctx) =>
+ ctx.selectedHit?.path === path && ctx.selectedHit.directory == directory && ctx.selectedHit.line === line.number,
+ );
+
+ return (
+
+
+ {line.number}
+
+
+
+
+
+ );
+}
+
+function Hit({ file }: { file: File }): ReactNode {
+ const filePath = `${file.directory}/${file.path}`;
+ return (
+
+
+ {filePath}
+
+ {file.lines && (
+
+ {file.lines.map((line, i, arr) => (
+
+ {arr[i - 1]?.number < line.number - 1 && }
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export function Search(): ReactNode {
+ const resultState = useSearchContext((ctx) => ctx.searchResult);
+ if (resultState == null) return null;
+
+ const { loading, error, result } = resultState;
+ if (loading) return Loading...
;
+ if (error)
+ return (
+
+ Error: {error.message}
+
+ );
+
+ return (
+
+ {result!.files.map((file) => (
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/App/pages/store/context.ts b/frontend/src/App/pages/store/context.ts
new file mode 100644
index 0000000..460a81c
--- /dev/null
+++ b/frontend/src/App/pages/store/context.ts
@@ -0,0 +1,17 @@
+import type { Context, Dispatch } from 'react';
+import type { ActionData, State } from '.';
+import { createContext, useContextSelector } from 'use-context-selector';
+
+export const internal = {
+ context: createContext(undefined),
+ searchContextDispatchRef: undefined as Dispatch | undefined,
+};
+
+export function useSearchContext(selector: (s: State) => T): T {
+ return useContextSelector(internal.context as Context, selector);
+}
+
+export function dispatch(actionData: ActionData): void {
+ if (!internal.searchContextDispatchRef) throw new Error('Search context dispatch not set');
+ internal.searchContextDispatchRef(actionData);
+}
diff --git a/frontend/src/App/pages/store/index.ts b/frontend/src/App/pages/store/index.ts
new file mode 100644
index 0000000..60466bb
--- /dev/null
+++ b/frontend/src/App/pages/store/index.ts
@@ -0,0 +1,77 @@
+import type { UseFormReturn } from 'react-hook-form';
+
+// Without setting a string value the reducer fails with https://github.com/microsoft/TypeScript/issues/28102 :(
+export enum ACTION {
+ SET_SEARCH_RESULT = 'SET_SEARCH_RESULT',
+ SET_FILE_RESULT = 'SET_FILE_RESULT',
+
+ SELECT_PREVIOUS = 'SELECT_PREVIOUS',
+ SELECT_NEXT = 'SELECT_NEXT',
+ CALLBACK_SELECTED_HIT = 'CALLBACK_SELECTED_HIT',
+}
+
+export type Range = [number, number];
+export type Line = { line: string; number: number; range?: Range };
+export type FileHeader = {
+ path: string;
+ directory: string;
+ repository: string;
+ branch: string;
+ range?: Range;
+};
+export type File = FileHeader & {
+ lines?: Line[];
+};
+export type SearchResult = {
+ files: File[];
+ truncated: boolean;
+ hits: number;
+ matchedFiles: number;
+ updatedAt: number;
+};
+export type LineMatch = { line: number; range: Range };
+export type FileResult = FileHeader & {
+ content: string;
+ matches: LineMatch[];
+ updatedAt: number;
+};
+
+export type ListResult = Partial & {
+ files: string[];
+ directories: string[];
+ updatedAt: number;
+};
+
+type HttpResultState = {
+ loading: boolean;
+ error?: { message: string };
+ result?: T;
+};
+export type SelectedHit = FileHeader & {
+ line: number;
+};
+
+export type State = {
+ form: UseFormReturn;
+ searchResult?: HttpResultState;
+ fileResult?: HttpResultState;
+ selectedHit?: SelectedHit;
+};
+
+export type Filters = {
+ query: string;
+ file: string;
+ excludeFile: string;
+ caseInsensitive: boolean;
+ numLinesBefore: number;
+ numLinesAfter: number;
+};
+
+export type ActionData =
+ | [ACTION.SELECT_PREVIOUS | ACTION.SELECT_NEXT]
+ | [ACTION.CALLBACK_SELECTED_HIT, (hit: SelectedHit) => void]
+ | [ACTION.SET_SEARCH_RESULT, HttpResultState | undefined]
+ | [ACTION.SET_FILE_RESULT, HttpResultState | undefined];
+
+export { useSearchContext, dispatch } from './context';
+export { SearchContextProvider } from './provider';
diff --git a/frontend/src/App/pages/store/provider.tsx b/frontend/src/App/pages/store/provider.tsx
new file mode 100644
index 0000000..959b5cf
--- /dev/null
+++ b/frontend/src/App/pages/store/provider.tsx
@@ -0,0 +1,113 @@
+import type { PropsWithChildren, ReactNode } from 'react';
+import { useLayoutEffect, useReducer, useRef } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { ACTION, dispatch, type ListResult } from '.';
+import type { SearchResult, Filters, FileResult, File } from '.';
+import { reducer } from './reducer';
+import { parseUrlParams } from './url-params';
+import { Get } from '../../libs/fetcher';
+import { useForm } from 'react-hook-form';
+import { internal } from './context';
+
+const SITE_TITLE = 'Code Search';
+
+function normalizeError(error: unknown): { message: string } {
+ if (error instanceof Error) return { message: error.message };
+ if (typeof error === 'string') return { message: error };
+ return { message: 'Unknown error' };
+}
+
+function listResultToSearchResult(result: ListResult): SearchResult {
+ const pathPrefix = result.path ? `${result.path}/` : '';
+ const directory = result.directory ?? '';
+ const repository = result.repository ?? '';
+ const branch = result.branch ?? '';
+ const files: File[] = [...result.directories.map((d) => d + '/'), ...result.files].map((f) => ({
+ path: `${pathPrefix}${f}`,
+ directory,
+ repository,
+ branch,
+ }));
+ return { files, hits: 0, matchedFiles: files.length, truncated: false, updatedAt: result.updatedAt };
+}
+
+function setPageTitle(filters: Filters): void {
+ const parts: string[] = [];
+ if (filters.query) parts.push(filters.query);
+ if (filters.file) parts.push(`file:${filters.file}`);
+ if (filters.excludeFile) parts.push(`-file:${filters.excludeFile}`);
+
+ document.title = parts.length > 0 ? `${parts.join(' ')} - ${SITE_TITLE}` : SITE_TITLE;
+}
+
+export function SearchContextProvider({ children }: PropsWithChildren): ReactNode {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const form = useForm({
+ defaultValues: parseUrlParams(location.search),
+ shouldUnregister: true,
+ });
+ const queryRef = useRef(undefined);
+
+ const [value, searchContextDispatch] = useReducer(reducer, { form });
+
+ useLayoutEffect(() => {
+ internal.searchContextDispatchRef = searchContextDispatch;
+ return (): void => (internal.searchContextDispatchRef = undefined);
+ }, []);
+
+ // Every time the URL changes, update the state
+ useLayoutEffect(() => {
+ if (location.pathname === '/') {
+ setPageTitle(parseUrlParams(location.search));
+ const queryParams = location.search;
+ if (queryRef.current === queryParams) return;
+ queryRef.current = queryParams;
+
+ dispatch([ACTION.SET_FILE_RESULT, undefined]);
+ if (queryParams === '') {
+ dispatch([ACTION.SET_SEARCH_RESULT, undefined]);
+ return;
+ }
+
+ dispatch([ACTION.SET_SEARCH_RESULT, { loading: true }]);
+ void Get(`/rest/search${queryParams}`)
+ .then((result) => ({ loading: false, result }))
+ .catch((error: unknown) => ({ loading: false, error: normalizeError(error) }))
+ .then((data) => {
+ dispatch([ACTION.SET_SEARCH_RESULT, data]);
+ });
+ } else if (location.pathname.startsWith('/file/')) {
+ document.title = `${location.pathname.substring(6)} - ${SITE_TITLE}`;
+ const params = new URLSearchParams(location.search);
+ params.set('p', location.pathname.substring(6));
+
+ dispatch([ACTION.SET_FILE_RESULT, { loading: true }]);
+ void Get(`/rest/file?${params.toString()}`)
+ .then((result) => ({ loading: false, result }))
+ .catch((error: unknown) => ({ loading: false, error: normalizeError(error) }))
+ .then((data) => {
+ dispatch([ACTION.SET_FILE_RESULT, data]);
+ });
+ } else if (location.pathname.startsWith('/list/')) {
+ document.title = `${location.pathname.substring(6)} - ${SITE_TITLE}`;
+ const params = new URLSearchParams(location.search);
+ params.set('p', location.pathname.substring(6));
+
+ dispatch([ACTION.SET_SEARCH_RESULT, { loading: true }]);
+ void Get(`/rest/list?${params.toString()}`)
+ .then((result) => ({ loading: false, result: listResultToSearchResult(result) }))
+ .catch((error: unknown) => ({ loading: false, error: normalizeError(error) }))
+ .then((data) => {
+ dispatch([ACTION.SET_SEARCH_RESULT, data]);
+ });
+ } else {
+ document.title = SITE_TITLE;
+ void navigate('/', { replace: true });
+ dispatch([ACTION.SET_SEARCH_RESULT, undefined]);
+ dispatch([ACTION.SET_FILE_RESULT, undefined]);
+ }
+ }, [navigate, location.pathname, location.search]);
+
+ return {children};
+}
diff --git a/frontend/src/App/pages/store/reducer.ts b/frontend/src/App/pages/store/reducer.ts
new file mode 100644
index 0000000..66aa941
--- /dev/null
+++ b/frontend/src/App/pages/store/reducer.ts
@@ -0,0 +1,87 @@
+import type { ActionData, File, SelectedHit, State } from '.';
+import { ACTION } from '.';
+
+function* hitIterator(files: File[]): Generator {
+ for (const file of files) {
+ for (const line of file.lines ?? []) {
+ if (line.range != null)
+ yield {
+ path: file.path,
+ directory: file.directory,
+ repository: file.repository,
+ branch: file.branch,
+ line: line.number,
+ };
+ }
+ }
+}
+
+function selectNode(state: State, down: boolean): State {
+ const files = state.searchResult?.result?.files;
+ if (files == null || files.length === 0) {
+ if (state.selectedHit == null) return state;
+ return { ...state, selectedHit: undefined };
+ }
+
+ const selected = state.selectedHit;
+ if (selected == null) return { ...state, selectedHit: hitIterator(files).next().value };
+
+ let select = undefined;
+ let prevHit = undefined;
+ let stopAtNext = false;
+ for (const hit of hitIterator(files)) {
+ select ??= prevHit = hit;
+ if (hit.path === selected.path && hit.directory == selected.directory && hit.line === selected.line) {
+ if (!down) {
+ select = prevHit;
+ break;
+ } else stopAtNext = true;
+ } else if (stopAtNext) {
+ select = hit;
+ break;
+ }
+ prevHit = hit;
+ }
+ return { ...state, selectedHit: select };
+}
+
+export function reducer(state: State, actionData: ActionData): State {
+ const result = _preReducer(state, actionData);
+ return Object.freeze(_postReducer(state, result));
+}
+
+function _postReducer(state: State, result: State): State {
+ if (state === result) return result; // Short circuit if pre-reducer produced no change
+ return result;
+}
+
+function _preReducer(state: State, [action, data]: ActionData): State {
+ switch (action) {
+ case ACTION.SELECT_PREVIOUS:
+ return selectNode(state, false);
+ case ACTION.SELECT_NEXT:
+ return selectNode(state, true);
+
+ case ACTION.SET_SEARCH_RESULT:
+ return selectNode({ ...state, searchResult: data, fileResult: undefined }, true);
+ case ACTION.SET_FILE_RESULT: {
+ const match = /^#L(\d+)$/.exec(window.location.hash);
+ const selectedHit = data?.result
+ ? {
+ path: data.result.path,
+ directory: data.result.directory,
+ repository: data.result.repository,
+ branch: data.result.branch,
+ line: match ? parseInt(match[1], 10) : 0,
+ }
+ : undefined;
+ return { ...state, fileResult: data, selectedHit };
+ }
+ case ACTION.CALLBACK_SELECTED_HIT:
+ if (state.selectedHit) data(state.selectedHit);
+ return state;
+
+ default:
+ throw new Error(`Unknown action ${JSON.stringify(action)}`);
+ }
+}
diff --git a/frontend/src/App/pages/store/url-params.ts b/frontend/src/App/pages/store/url-params.ts
new file mode 100644
index 0000000..23feaae
--- /dev/null
+++ b/frontend/src/App/pages/store/url-params.ts
@@ -0,0 +1,43 @@
+import type { Filters } from '.';
+
+const qpQuery = 'q';
+const qpFile = 'f';
+const qpExcludeFile = 'xf';
+const qpCaseInsensitive = 'i';
+const qpNumBeforeLines = 'b';
+const qpNumAfterLines = 'a';
+
+export function parseUrlParams(search: string): Filters {
+ const urlParams = new URLSearchParams(search);
+ const parseNum = (param: string, defaultValue: number): number => {
+ const val = parseInt(urlParams.get(param) ?? '', 10);
+ return isNaN(val) ? defaultValue : val;
+ };
+ return {
+ query: urlParams.get(qpQuery) ?? '',
+ file: urlParams.get(qpFile) ?? '',
+ excludeFile: urlParams.get(qpExcludeFile) ?? '',
+ caseInsensitive: urlParams.get(qpCaseInsensitive) === 'true',
+ numLinesBefore: parseNum(qpNumBeforeLines, 0),
+ numLinesAfter: parseNum(qpNumAfterLines, 0),
+ };
+}
+
+export function createUrlParams({
+ query,
+ file,
+ excludeFile,
+ caseInsensitive,
+ numLinesBefore,
+ numLinesAfter,
+}: Filters): string {
+ const params = new URLSearchParams();
+ if (query) params.set(qpQuery, query);
+ if (file) params.set(qpFile, file);
+ if (excludeFile) params.set(qpExcludeFile, excludeFile);
+ if (caseInsensitive) params.set(qpCaseInsensitive, 'true');
+ if (numLinesBefore !== 0) params.set(qpNumBeforeLines, numLinesBefore.toString(10));
+ if (numLinesAfter !== 0) params.set(qpNumAfterLines, numLinesAfter.toString(10));
+ const paramsStr = params.toString();
+ return paramsStr.length > 0 ? '?' + paramsStr : '';
+}
diff --git a/frontend/src/favicon.svg b/frontend/src/favicon.svg
new file mode 100644
index 0000000..937582b
--- /dev/null
+++ b/frontend/src/favicon.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..6045ee8
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,103 @@
+@import "tailwindcss";
+
+/* Highlight styles */
+code[class*="language-"], pre[class*="language-"] {
+ background: none;
+ text-shadow: 0 1px white;
+ font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+ white-space: pre;
+ tab-size: 4;
+ hyphens: none;
+ margin-top: -2px;
+}
+
+pre[class*="language-"]::selection, pre[class*="language-"] ::selection {
+ text-shadow: none;
+ background: #b3d4fc;
+}
+
+.line {
+ height: calc(var(--spacing) * 6);
+ padding-inline: calc(var(--spacing) * 2);
+}
+
+.line.highlight {
+ background: #ffffc0;
+}
+
+.highlight {
+ background: #ffff00;
+}
+
+/* Code blocks */
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+ color: slategray;
+}
+
+.token.punctuation {
+ color: #999;
+}
+
+.token.namespace {
+ opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+ color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+ color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+ color: #9a6e3a;
+ background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+ color: #07a;
+}
+
+.token.function,
+.token.class-name {
+ color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+ color: #e90;
+}
+
+.token.important,
+.token.bold {
+ font-weight: bold;
+}
+.token.italic {
+ font-style: italic;
+}
+
+.token.entity {
+ cursor: help;
+}
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
new file mode 100755
index 0000000..e0670de
--- /dev/null
+++ b/frontend/src/index.tsx
@@ -0,0 +1,5 @@
+import ReactDOM from 'react-dom/client';
+import App from './App/index';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render();
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..3fb90f0
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "jsx": "react-jsx",
+ "skipLibCheck": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "strict": true,
+ "noEmit": true,
+
+ "baseUrl": "."
+ },
+
+ "include": [
+ "src",
+ "eslint.config.ts",
+ "vite.config.ts"
+ ],
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..7ffef2a
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,21 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig as defineViteConfig, mergeConfig } from 'vite';
+import { defineConfig as defineVitestConfig } from 'vitest/config';
+import tailwindcss from '@tailwindcss/vite';
+
+const viteConfig = defineViteConfig({
+ build: {
+ sourcemap: false,
+ },
+ plugins: [react(), tailwindcss()],
+});
+
+const vitestConfig = defineVitestConfig({
+ test: {
+ environment: 'happy-dom',
+ globals: true,
+ watch: false,
+ },
+});
+
+export default mergeConfig(viteConfig, vitestConfig);
diff --git a/go.mod b/go.mod
index dc2e7d2..0ca39e9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module github.com/google/codesearch
+module github.com/freva/codesearch
-go 1.23
+go 1.25.6
diff --git a/index/read.go b/index/read.go
index 1c8715b..a3bc00c 100644
--- a/index/read.go
+++ b/index/read.go
@@ -664,7 +664,10 @@ func mmap(file string) mmapData {
// File returns the name of the index file to use.
// It is either $CSEARCHINDEX or $HOME/.csearchindex.
-func File() string {
+func File(file string) string {
+ if file != "" {
+ return file
+ }
f := os.Getenv("CSEARCHINDEX")
if f != "" {
return f
diff --git a/index/write.go b/index/write.go
index 185ed23..787db95 100644
--- a/index/write.go
+++ b/index/write.go
@@ -16,7 +16,7 @@ import (
"slices"
"strings"
- "github.com/google/codesearch/sparse"
+ "github.com/freva/codesearch/sparse"
)
// Index writing. See read.go for details of on-disk format.
@@ -357,7 +357,9 @@ func (ix *IndexWriter) Flush() {
os.Remove(ix.nameIndex.name)
os.Remove(ix.postIndex.name)
- log.Printf("%d data bytes, %d index bytes", ix.totalBytes, ix.main.Offset())
+ if ix.Verbose {
+ log.Printf("%d data bytes, %d index bytes", ix.totalBytes, ix.main.Offset())
+ }
ix.main.Flush()
}
@@ -429,7 +431,9 @@ func (ix *IndexWriter) mergePost(out *Buffer) {
var h postHeap
if len(ix.postEnds) > 0 {
- log.Printf("merge mem + %d MB disk", ix.postEnds[len(ix.postEnds)-1]>>20)
+ if ix.Verbose {
+ log.Printf("merge mem + %d MB disk", ix.postEnds[len(ix.postEnds)-1]>>20)
+ }
h.addFile(ix.postFile, ix.postEnds)
}
sortPost(ix.post)
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..0d6632a
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,320 @@
+package config
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type Include struct {
+ Owner string
+ Name string
+ Ref string // The branch or commit
+}
+
+func (in Include) String() string {
+ if in.Name == "" {
+ return in.Owner // Just the owner
+ }
+ if in.Ref == "" {
+ return fmt.Sprintf("%s/%s", in.Owner, in.Name) // Owner/Repo
+ }
+ return fmt.Sprintf("%s/%s#%s", in.Owner, in.Name, in.Ref) // Owner/Repo#Ref
+}
+
+// Server holds configuration for a git server
+type Server struct {
+ Name string
+ ApiURL string
+ CloneURL string
+ WebURL string
+ Token string
+ Exclude string
+ Include []Include
+}
+
+// Config is the top-level struct holding all parsed configuration
+type Config struct {
+ // --- Public Fields ---
+
+ // Global settings
+ CodeDir string
+ CodeIndexPath string
+ FileIndexPath string
+ FileListsDir string
+ ManifestPath string
+ Port int
+
+ // Sections
+ Servers map[string]*Server
+
+ // --- Private Fields ---
+ configPath string // Absolute path to the config file
+ configFileDir string // Directory of the config file
+ workDir string // Directory to resolve all relative paths in config against
+}
+
+// Repository represents a resolved repository with its source details.
+type Repository struct {
+ Server string `json:"server"` // Name of the server from Config.Servers
+ Owner string `json:"owner"` // GitHub owner (name of org or user)
+ Name string `json:"name"` // Name of the repository
+ Branch string `json:"branch"` // Branch (or commit if Include.Ref was a commit) to check out
+ Commit string `json:"commit"` // Commit hash to check out
+}
+
+func (r Repository) RepoDir() string {
+ return fmt.Sprintf("%s/%s/%s", r.Server, r.Owner, r.Name)
+}
+
+type Manifest struct {
+ Servers map[string]string `json:"servers"` // Server API URL by server name
+ Repositories map[string]*Repository `json:"repositories"` // Repository details by path prefix
+ UpdatedAt time.Time `json:"updated_at"` // Timestamp of the last update
+}
+
+var includePattern = regexp.MustCompile(`^[a-zA-Z0-9-]+(?:/[a-zA-Z0-9-._]+(?:#\S+)?)?$`)
+
+func ReadManifest(path string) (*Manifest, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read manifest at '%s': %w", path, err)
+ }
+
+ var manifest *Manifest
+ if err := json.Unmarshal(data, &manifest); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal manifest at '%s': %w", path, err)
+ }
+ return manifest, nil
+}
+
+// ReadConfig parses Config from the given path.
+func ReadConfig(path string) (*Config, error) {
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get absolute path for '%s': %w", path, err)
+ }
+ if _, err := os.Stat(absPath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("config file does not exist: %s", absPath)
+ }
+
+ file, err := os.Open(absPath)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ var c = &Config{
+ Servers: make(map[string]*Server),
+ Port: 80, // Default port
+ configPath: absPath,
+ configFileDir: filepath.Dir(absPath),
+ }
+ err = c.parseConfig(file)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing config file '%s': %w", absPath, err)
+ }
+ return c, nil
+}
+
+func (c *Config) parseConfig(file *os.File) error {
+ // Regex for parsing section headers, e.g., [server github]
+ sectionRegex := regexp.MustCompile(`^\s*\[\s*(server)\s+([^]]+)\s*]`)
+ // Regex for parsing key-value pairs, e.g., workdir = /path/to/db
+ assignRegex := regexp.MustCompile(`^\s*([a-zA-Z0-9_.-]+)\s*=\s*(.+)`)
+
+ var currentServer *Server
+
+ scanner := bufio.NewScanner(file)
+ lineNum := 0
+ for scanner.Scan() {
+ lineNum++
+ line := strings.TrimSpace(scanner.Text())
+
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue // Skip empty lines and comments
+ }
+
+ loc := fmt.Sprintf("%s:%d", filepath.Base(c.configPath), lineNum)
+
+ if matches := sectionRegex.FindStringSubmatch(line); len(matches) == 3 {
+ // A new section has started, reset current context
+ currentServer = nil
+
+ sectionType := matches[1]
+ sectionName := strings.TrimSpace(matches[2])
+
+ switch sectionType {
+ case "server":
+ currentServer = &Server{Name: sectionName, ApiURL: "https://api.github.com", WebURL: "https://github.com", CloneURL: "https://github.com"}
+ c.Servers[sectionName] = currentServer
+ }
+ } else if matches := assignRegex.FindStringSubmatch(line); len(matches) == 3 {
+ key := matches[1]
+ value := strings.TrimSpace(matches[2])
+
+ // Are we in a server section?
+ if currentServer != nil {
+ if err := c.parseServerVar(currentServer, key, value, loc); err != nil {
+ return err
+ }
+ } else {
+ // Global variable
+ if err := c.parseGlobalVar(key, value, loc); err != nil {
+ return err
+ }
+ }
+ } else {
+ return fmt.Errorf("%s: invalid line format: %s", loc, scanner.Text())
+ }
+ }
+
+ for name, server := range c.Servers {
+ if server.CloneURL == "" {
+ return fmt.Errorf("%s: server '%s' missing required 'url' setting", c.configPath, name)
+ }
+ }
+
+ if c.workDir == "" {
+ return fmt.Errorf("%s: missing required 'workdir' setting", c.configPath)
+ }
+ if c.CodeDir == "" {
+ c.CodeDir = filepath.Join(c.workDir, "code")
+ }
+ if c.FileIndexPath == "" {
+ c.FileIndexPath = filepath.Join(c.workDir, "csearch.fileindex")
+ }
+ if c.FileListsDir == "" {
+ c.FileListsDir = filepath.Join(c.workDir, "filelists")
+ }
+ if c.CodeIndexPath == "" {
+ c.CodeIndexPath = filepath.Join(c.workDir, "csearch.index")
+ }
+ if c.ManifestPath == "" {
+ c.ManifestPath = filepath.Join(c.workDir, "manifest.json")
+ }
+
+ return scanner.Err()
+}
+
+// Help provides the help text describing the config file format.
+func Help() string {
+ return `The config file has the following format:
+
+ config-file: global-section section*
+ global-section: assign*
+ section: '[' name S value ']' assign*
+ assign: key '=' value
+
+Global settings:
+ 'code': Directory to contain source to check out and index. [workdir/code]
+ 'fileindex': Path to file index file. [workdir/csearch.fileindex]
+ 'index': Path to codesearch index file. [workdir/csearch.index]
+ 'port': Port cserver should listen to. [80]
+ 'manifest': Path to the manifest file. [workdir/manifest.json]
+ 'workdir': The working directory owned and managed by this program. Required.
+Relative paths are resolved relative to the config file.
+The 'server' section names a GitHub server and allows these settings:
+ 'api': URL to GitHub REST API. [https://api.github.com]
+ 'exclude': Excludes all ORG/REPO matching the regex. At most 1.
+ 'include': Either
+ OWNER - user/organisation name, will check out all of their repositories, or
+ OWNER/REPO - a specific repository, or
+ OWNER/REPO#BRANCH - a specific repository at a specific branch, or
+ OWNER/REPO#REF - a specific repository at a specific commit.
+ 'token': An OAuth2 token, e.g. a personal access token.
+ 'weburl': URL to the web interface of the server. [https://github.com]
+ 'url': Base URL for cloning. [https://github.com]`
+}
+
+func (c *Config) parseGlobalVar(key, value, loc string) (err error) {
+ switch key {
+ case "code":
+ c.CodeDir, err = c.resolvePath(value)
+ case "fileindex":
+ c.FileIndexPath, err = c.resolvePath(value)
+ case "filelists":
+ c.FileListsDir, err = c.resolvePath(value)
+ case "index":
+ c.CodeIndexPath, err = c.resolvePath(value)
+ case "manifest":
+ c.ManifestPath, err = c.resolvePath(value)
+ case "workdir":
+ c.workDir, err = c.resolvePath(value)
+ case "port":
+ c.Port, err = strconv.Atoi(value)
+ if err != nil {
+ return fmt.Errorf("%s: invalid port number: %s", loc, value)
+ }
+ default:
+ return fmt.Errorf("%s: unknown global configuration key: '%s'", loc, key)
+ }
+ return err
+}
+
+// resolvePath returns absolute path, relative paths are resolved relative to config file.
+func (c *Config) resolvePath(p string) (string, error) {
+ if strings.HasPrefix(p, "~") {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ p = filepath.Join(home, p[1:])
+ }
+ if !filepath.IsAbs(p) {
+ p = filepath.Join(c.configFileDir, p)
+ }
+ return filepath.Clean(p), nil
+}
+
+func (c *Config) parseServerVar(s *Server, key, value, loc string) error {
+ switch key {
+ case "api":
+ s.ApiURL = value
+ case "url":
+ s.CloneURL = value
+ case "weburl":
+ s.WebURL = value
+ case "token":
+ s.Token = value
+ case "exclude":
+ _, err := regexp.Compile(value)
+ if err != nil {
+ return fmt.Errorf("%s: invalid regex for 'exclude': %w", loc, err)
+ }
+ s.Exclude = value
+ case "include":
+ if !includePattern.MatchString(value) {
+ return fmt.Errorf("%s: invalid include format: %s", loc, value)
+ }
+
+ // Check for a ref/branch part
+ var owner, name, ref string
+ if strings.Contains(value, "#") {
+ parts := strings.SplitN(value, "#", 2)
+ value = parts[0]
+ ref = parts[1]
+ }
+
+ // Check for owner/name part
+ if strings.Contains(value, "/") {
+ parts := strings.SplitN(value, "/", 2)
+ owner = parts[0]
+ name = parts[1]
+ } else {
+ // If no slash, it's an owner-only include
+ owner = value
+ }
+
+ s.Include = append(s.Include, Include{Owner: owner, Name: name, Ref: ref})
+ default:
+ return fmt.Errorf("%s: unknown key '%s' in server '%s'", loc, key, s.Name)
+ }
+ return nil
+}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..51a8aed
--- /dev/null
+++ b/internal/config/config_test.go
@@ -0,0 +1,208 @@
+package config
+
+import (
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+// Helper function to create a temporary config file for testing
+func createTempConfigFile(t *testing.T, content string) string {
+ tempDir := t.TempDir()
+ filePath := filepath.Join(tempDir, "test_config.conf")
+ err := os.WriteFile(filePath, []byte(content), 0644)
+ if err != nil {
+ t.Fatalf("Failed to create temporary config file: %v", err)
+ }
+ return filePath
+}
+
+func assertEqualConfig(t *testing.T, configPath string, expected *Config) {
+ cfg, err := ReadConfig(configPath)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+ if !reflect.DeepEqual(cfg, expected) {
+ t.Errorf("Config mismatch:\nExpected: %+v\nGot: %+v", expected, cfg)
+ }
+}
+
+func assertConfigError(t *testing.T, content, errorMessage string) {
+ configPath := createTempConfigFile(t, content)
+ _, err := ReadConfig(configPath)
+ if err == nil {
+ t.Fatal("Expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), errorMessage) {
+ t.Errorf("Expected error to contain '%s', got '%s'", err, err.Error())
+ }
+}
+
+func TestReadConfig(t *testing.T) {
+ t.Run("valid minimal config file", func(t *testing.T) {
+ content := `
+workdir = ./data
+
+[server github]
+exclude = ^my-org/private-.*
+include = my-org/public-repo#main
+include = another-org/repo2
+include = single-owner
+
+[server internal]
+api = https://api.git.example.com
+token = my-secret-token
+url = https://my-secret-token@git.example.com
+weburl = https://git.example.com
+include = internal-org/internal-repo
+`
+ configPath := createTempConfigFile(t, content)
+ configFileDir := filepath.Dir(configPath)
+ expected := &Config{
+ CodeDir: path.Join(configFileDir, "data/code"),
+ CodeIndexPath: path.Join(configFileDir, "data/csearch.index"),
+ FileIndexPath: path.Join(configFileDir, "data/csearch.fileindex"),
+ FileListsDir: path.Join(configFileDir, "data/filelists"),
+ ManifestPath: path.Join(configFileDir, "data/manifest.json"),
+ Port: 80,
+ Servers: map[string]*Server{
+ "github": {
+ Name: "github",
+ ApiURL: "https://api.github.com",
+ CloneURL: "https://github.com",
+ WebURL: "https://github.com",
+ Exclude: "^my-org/private-.*",
+ Include: []Include{
+ {Owner: "my-org", Name: "public-repo", Ref: "main"},
+ {Owner: "another-org", Name: "repo2"},
+ {Owner: "single-owner"},
+ },
+ },
+ "internal": {
+ Name: "internal",
+ ApiURL: "https://api.git.example.com",
+ CloneURL: "https://my-secret-token@git.example.com",
+ WebURL: "https://git.example.com",
+ Token: "my-secret-token",
+ Exclude: "",
+ Include: []Include{{Owner: "internal-org", Name: "internal-repo"}},
+ },
+ },
+ configPath: configPath,
+ configFileDir: configFileDir,
+ workDir: path.Join(configFileDir, "data"),
+ }
+ assertEqualConfig(t, configPath, expected)
+ })
+
+ t.Run("file paths override", func(t *testing.T) {
+ content := `
+code=/absolute/path/to/code
+index=c.idx
+fileindex = fileindex.idx
+filelists = data/../filelists
+manifest = mf.json
+port=1234
+workdir =~/data
+`
+ configPath := createTempConfigFile(t, content)
+ configFileDir := filepath.Dir(configPath)
+ home, _ := os.UserHomeDir()
+ expected := &Config{
+ CodeDir: "/absolute/path/to/code",
+ CodeIndexPath: path.Join(configFileDir, "c.idx"),
+ FileIndexPath: path.Join(configFileDir, "fileindex.idx"),
+ FileListsDir: path.Join(configFileDir, "filelists"),
+ ManifestPath: path.Join(configFileDir, "mf.json"),
+ Port: 1234,
+ Servers: map[string]*Server{},
+ configPath: configPath,
+ configFileDir: configFileDir,
+ workDir: path.Join(home, "data"),
+ }
+ assertEqualConfig(t, configPath, expected)
+ })
+
+ t.Run("config file does not exist", func(t *testing.T) {
+ _, err := ReadConfig("non_existent_config.conf")
+ if err == nil {
+ t.Fatal("Expected error for non-existent file, got nil")
+ }
+ if !strings.Contains(err.Error(), "config file does not exist") {
+ t.Errorf("Expected 'config file does not exist' error, got %v", err)
+ }
+ })
+
+ t.Run("missing required global settings", func(t *testing.T) {
+ content := `
+# Missing workdir
+[server github]
+url = https://github.com
+weburl = https://github.com
+`
+ assertConfigError(t, content, "missing required 'workdir' setting")
+ })
+
+ t.Run("invalid line format", func(t *testing.T) {
+ content := `
+workdir = ./data
+invalid line
+`
+ assertConfigError(t, content, "invalid line format: invalid line")
+ })
+
+ t.Run("invalid port number", func(t *testing.T) {
+ content := `
+port = abc
+workdir = ./data
+`
+ assertConfigError(t, content, "invalid port number: abc")
+ })
+
+ t.Run("unknown global key", func(t *testing.T) {
+ content := `
+unknown_global = value
+workdir = ./data
+`
+ assertConfigError(t, content, ":2: unknown global configuration key: 'unknown_global'")
+ })
+
+ t.Run("unknown server key", func(t *testing.T) {
+ content := `
+workdir = ./data
+[server test]
+api = test
+url = test
+weburl = test
+unknown_server_key = value
+`
+ assertConfigError(t, content, ":7: unknown key 'unknown_server_key' in server 'test'")
+ })
+
+ t.Run("invalid exclude regex", func(t *testing.T) {
+ content := `
+workdir = ./data
+[server test]
+api = test
+url = test
+weburl = test
+exclude = [invalid regex
+`
+ assertConfigError(t, content, "invalid regex for 'exclude': error parsing regexp: missing closing ]: `[invalid regex`")
+ })
+
+ t.Run("invalid include format", func(t *testing.T) {
+ content := `
+workdir = ./data
+[server test]
+api = test
+url = test
+weburl = test
+include = invalid/format/with/too/many/slashes
+`
+ assertConfigError(t, content, "invalid include format: invalid/format/with/too/many/slashes")
+ })
+}
diff --git a/lib/README.template b/lib/README.template
deleted file mode 100644
index 16b70b9..0000000
--- a/lib/README.template
+++ /dev/null
@@ -1,15 +0,0 @@
-These are the command-line Code Search tools from
-https://github.com/google/codesearch.
-
-These binaries are for ARCH systems running OPERSYS.
-
-To get started, run cindex with a list of directories to index:
-
- cindex /usr/include $HOME/src
-
-Then run csearch to run grep over all the indexed sources:
-
- csearch DATAKIT
-
-For details, run either command with the -help option, and
-read http://swtch.com/~rsc/regexp/regexp4.html.
diff --git a/lib/buildall b/lib/buildall
deleted file mode 100755
index 947250e..0000000
--- a/lib/buildall
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-
-# This script builds the code search binaries for a variety of OS/architecture combinations.
-
-. ./setup
-
-for i in {5,6,8}{c,g,a,l}
-do
- go tool dist install cmd/$i
-done
-
-build() {
- echo "# $1"
- goos=$(echo $1 | sed 's;/.*;;')
- goarch=$(echo $1 | sed 's;.*/;;')
- GOOS=$goos GOARCH=$goarch CGO_ENABLED=0 \
- go install -a code.google.com/p/codesearch/cmd/{cgrep,cindex,csearch}
- rm -rf codesearch-$version
- mkdir codesearch-$version
- mv ~/g/bin/{cgrep,cindex,csearch}* codesearch-$version
- chmod +x codesearch-$version/*
- cat README.template | sed "s/ARCH/$(arch $goarch)/; s/OPERSYS/$(os $goos)/" >codesearch-$version/README.txt
- rm -f codesearch-$version-$goos-$goarch.zip
- zip -z -r codesearch-$version-$goos-$goarch.zip codesearch-$version < codesearch-$version/README.txt
- rm -rf codesearch-0.01
-}
-
-for i in {linux,darwin,freebsd,windows}/{amd64,386}
-do
- build $i
-done
diff --git a/lib/setup b/lib/setup
deleted file mode 100644
index d1db250..0000000
--- a/lib/setup
+++ /dev/null
@@ -1,23 +0,0 @@
-set -e
-
-os() {
- case "$1" in
- freebsd) echo FreeBSD;;
- linux) echo Linux;;
- darwin) echo Mac OS X;;
- openbsd) echo OpenBSD;;
- netbsd) echo NetBSD;;
- windows) echo Windows;;
- *) echo $1;;
- esac
-}
-
-arch() {
- case "$1" in
- 386) echo 32-bit x86;;
- amd64) echo 64-bit x86;;
- *) echo $1;;
- esac
-}
-
-version=$(cat version)
diff --git a/lib/uploadall b/lib/uploadall
deleted file mode 100644
index 8edd51a..0000000
--- a/lib/uploadall
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/sh
-
-# gcodeup is a copy of $GOROOT/misc/dashboard/googlecode_upload.py.
-
-. ./setup
-user=$(sed -n 's/^re2.username = //' ~/.hgrc)
-password=$(sed -n 's/^re2\.password = //' ~/.hgrc)
-
-upload() {
- goos=$(echo $1 | sed "s/codesearch-$version-//; s/-.*//")
- goarch=$(echo $1 | sed "s/codesearch-$version-//; s/[a-z0-9]*-//; s/-.*//")
- gcodeup -s "binaries for $(os $goos) $(arch $goarch)" -p codesearch -u "$user" -w "$password" codesearch-$version-$1-$2.zip
-}
-
-for i in codesearch-$version-*
-do
- upload $i
-done
diff --git a/lib/version b/lib/version
deleted file mode 100644
index 6e6566c..0000000
--- a/lib/version
+++ /dev/null
@@ -1 +0,0 @@
-0.01
diff --git a/regexp/match.go b/regexp/match.go
index 7a90d1a..d471ee9 100644
--- a/regexp/match.go
+++ b/regexp/match.go
@@ -17,7 +17,7 @@ import (
"strconv"
"strings"
- "github.com/google/codesearch/sparse"
+ "github.com/freva/codesearch/sparse"
)
// A matcher holds the state for running regular expression search.
@@ -351,6 +351,11 @@ func isWordByte(c int) bool {
c == '_'
}
+type LineHit struct {
+ Lineno int
+ Line string
+}
+
// TODO:
type Grep struct {
Regexp *Regexp // regexp to search for
@@ -373,6 +378,21 @@ type Grep struct {
PostContext int // number of lines to print before
buf []byte
+
+ // Note that there may be several matches per line, but we're ignorant.
+ MatchedLines []LineHit
+}
+
+func (g *Grep) initForMatching() {
+ if g.buf == nil {
+ g.buf = make([]byte, 1<<20)
+ }
+
+ if g.MatchedLines == nil {
+ g.MatchedLines = make([]LineHit, 0, 100)
+ } else {
+ g.MatchedLines = g.MatchedLines[:0]
+ }
}
func (g *Grep) AddFlags() {
@@ -414,6 +434,17 @@ func (g *Grep) File(name string) {
g.Reader(f, name)
}
+func (g *Grep) File2(name string) {
+ f, err := os.Open(name)
+ if err != nil {
+ fmt.Fprintf(g.Stderr, "%s\n", err)
+ g.initForMatching()
+ return
+ }
+ defer f.Close()
+ g.FindMatches(f, name)
+}
+
var nl = []byte{'\n'}
func countNL(b []byte) int {
@@ -646,3 +677,54 @@ func chomp(s []byte) []byte {
}
return s[:i]
}
+
+func (g *Grep) FindMatches(r io.Reader, name string) {
+ g.initForMatching()
+ var (
+ buf = g.buf[:0]
+ lineno = 1
+ beginText = true
+ endText = false
+ )
+ for {
+ n, err := io.ReadFull(r, buf[len(buf):cap(buf)])
+ buf = buf[:len(buf)+n]
+ end := len(buf)
+ if err == nil {
+ end = bytes.LastIndex(buf, nl) + 1
+ } else {
+ endText = true
+ }
+ chunkStart := 0
+ for chunkStart < end {
+ m1 := g.Regexp.Match(buf[chunkStart:end], beginText, endText) + chunkStart
+ beginText = false
+ if m1 < chunkStart {
+ break
+ }
+ g.Match = true
+ lineStart := bytes.LastIndex(buf[chunkStart:m1], nl) + 1 + chunkStart
+ lineEnd := m1 + 1
+ if lineEnd > end {
+ lineEnd = end
+ }
+ lineno += countNL(buf[chunkStart:lineStart])
+ line := buf[lineStart:lineEnd]
+ g.MatchedLines = append(g.MatchedLines, LineHit{
+ Lineno: lineno,
+ Line: string(line),
+ })
+ lineno++
+ chunkStart = lineEnd
+ }
+ lineno += countNL(buf[chunkStart:end])
+ n = copy(buf, buf[end:])
+ buf = buf[:n]
+ if len(buf) == 0 && err != nil {
+ if err != io.EOF && err != io.ErrUnexpectedEOF {
+ fmt.Fprintf(g.Stderr, "%s: %v\n", name, err)
+ }
+ break
+ }
+ }
+}
diff --git a/regexp/match2.go b/regexp/match2.go
new file mode 100644
index 0000000..5567186
--- /dev/null
+++ b/regexp/match2.go
@@ -0,0 +1,66 @@
+package regexp
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "iter"
+ "os"
+
+ "github.com/freva/codesearch/index"
+)
+
+type LineMatch struct {
+ Lineno int
+ Line string
+ Match bool
+}
+
+func FindMatches(name index.Path, regexp *Regexp, beforeLines int, afterLines int) iter.Seq[LineMatch] {
+ return func(yield func(match LineMatch) bool) {
+ file, err := os.Open(name.String())
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to open file: %v", err)
+ return
+ }
+ defer file.Close()
+
+ reader := bufio.NewReader(file)
+ var (
+ prevLines = NewFixedSizeQueue[string](beforeLines)
+ lineno = 1
+ printNextNLines = -1
+ eof = false
+ )
+
+ for !eof {
+ line, err := reader.ReadString('\n')
+ if err == io.EOF {
+ eof = true
+ } else if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to read line: %v", err)
+ return
+ }
+
+ m1 := regexp.MatchString(line, true, true)
+ if m1 >= 0 {
+ for i := prevLines.Size(); i > 0; i-- {
+ if !yield(LineMatch{Lineno: lineno - i, Line: prevLines.Dequeue(), Match: false}) {
+ return
+ }
+ }
+ printNextNLines = afterLines
+ }
+
+ if printNextNLines >= 0 {
+ if !yield(LineMatch{Lineno: lineno, Line: line, Match: printNextNLines == afterLines}) {
+ return
+ }
+ printNextNLines--
+ } else {
+ prevLines.Enqueue(line)
+ }
+ lineno++
+ }
+ }
+}
diff --git a/regexp/queue.go b/regexp/queue.go
new file mode 100644
index 0000000..2feb1a1
--- /dev/null
+++ b/regexp/queue.go
@@ -0,0 +1,50 @@
+package regexp
+
+type FixedSizeQueue[T any] struct {
+ data []T
+ capacity int
+ head int // Index of the oldest element
+ tail int // Index where the next element will be added
+ size int // Number of elements in the queue
+}
+
+func NewFixedSizeQueue[T any](capacity int) *FixedSizeQueue[T] {
+ return &FixedSizeQueue[T]{
+ data: make([]T, capacity),
+ capacity: capacity,
+ head: 0,
+ tail: 0,
+ size: 0,
+ }
+}
+
+func (q *FixedSizeQueue[T]) Enqueue(item T) {
+ if q.capacity == 0 {
+ return
+ } else if q.size == q.capacity {
+ // Queue is full, overwrite the oldest element
+ q.data[q.tail] = item
+ q.tail = (q.tail + 1) % q.capacity
+ q.head = (q.head + 1) % q.capacity
+ } else {
+ q.data[q.tail] = item
+ q.tail = (q.tail + 1) % q.capacity
+ q.size++
+ }
+}
+
+func (q *FixedSizeQueue[T]) Dequeue() T {
+ if q.size == 0 {
+ var zero T // Zero value for the type T
+ return zero
+ }
+
+ item := q.data[q.head]
+ q.head = (q.head + 1) % q.capacity
+ q.size--
+ return item
+}
+
+func (q *FixedSizeQueue[T]) Size() int {
+ return q.size
+}
diff --git a/systemd/codesearch-server.service b/systemd/codesearch-server.service
new file mode 100644
index 0000000..0e7e503
--- /dev/null
+++ b/systemd/codesearch-server.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Codesearch HTTP server
+
+[Service]
+ExecStart=%h/.go/bin/cserver --config %h/git/freva/codesearch/config
+Restart=always
+RestartSec=1
+
+[Install]
+WantedBy=default.target
diff --git a/systemd/codesearch-updater.service b/systemd/codesearch-updater.service
new file mode 100644
index 0000000..13f1af9
--- /dev/null
+++ b/systemd/codesearch-updater.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=Codesearch index updater
+
+[Service]
+ExecStart=%h/.go/bin/csupdater --config %h/git/freva/codesearch/config --exit-early
+Restart=always
+RestartSec=60
+
+[Install]
+WantedBy=default.target