From 71d45ed5edcae8a384f0a8e89b0b2b5bfb3ecc64 Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Mon, 24 Feb 2025 17:35:35 -0800 Subject: [PATCH 01/51] Tests/increase coverage (#45) * Adding some more test coverage. Lots more to be done, but good enough for now * adding github actions * fixing flaky test * ignoring unix timestamp, other coverage is fine * fixed the wrong test * adding network tests * adding test for RunOnce * adding devnull and stdout tests * Adding coveralls to CI * fixing Elastic HTTP test * fixing timezone extraction bug in the test * changing goveralls invocation method --- .github/workflows/ci.yml | 55 ++++ .travis.yml | 33 --- Makefile | 5 +- internal/config.go | 40 +++ internal/config_test.go | 171 +++++++++++- logger/logger.go | 4 +- logger/logger_test.go | 294 +++++++++++++++++++- run/runOnce.go | 50 +++- run/runonce_test.go | 126 +++++++++ template/template_test.go | 36 ++- tests/devnull_test.go | 120 ++++++++ tests/file_test.go | 16 +- tests/http_test.go | 305 +++++++++++++++++++-- tests/httpoutput/httpoutput.yml | 56 ---- tests/httpoutput/splunkoutput.yml | 56 ---- tests/network_test.go | 441 ++++++++++++++++++++++++++++++ tests/outputcache/outputcache.yml | 18 -- tests/outputcache_test.go | 32 ++- tests/replay/fullreplay.yml | 20 -- tests/replay_test.go | 29 +- tests/stdout_test.go | 211 ++++++++++++++ tests/timer/realtimereplay.yml | 13 + timer/timer_test.go | 72 +++++ 23 files changed, 1952 insertions(+), 251 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml create mode 100644 run/runonce_test.go create mode 100644 tests/devnull_test.go delete mode 100644 tests/httpoutput/httpoutput.yml delete mode 100644 tests/httpoutput/splunkoutput.yml create mode 100644 tests/network_test.go delete mode 100644 tests/outputcache/outputcache.yml delete mode 100644 tests/replay/fullreplay.yml create mode 100644 tests/stdout_test.go create mode 100644 tests/timer/realtimereplay.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fdf71c2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.22.x' + + - name: Install Dependencies + run: make GOBIN=$HOME/gopath/bin deps + + - name: Run Tests + run: make GOBIN=$HOME/gopath/bin test + + - name: Run Coverage Tests + env: + COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} + run: | + $HOME/gopath/bin/goveralls -v -service=github + + - name: Build Project + run: make GOBIN=$HOME/gopath/bin build + + - name: Build Docker Image + run: docker build -t clintsharp/gogen . + + # Deployment: These steps run only on the main branch. + # - name: Configure AWS Credentials + # if: github.ref == 'refs/heads/main' + # uses: aws-actions/configure-aws-credentials@v1 + # with: + # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # aws-region: us-west-1 + + # - name: Deploy Build Artifacts to S3 + # if: github.ref == 'refs/heads/main' + # run: aws s3 sync build s3://gogen-artifacts --delete + + # - name: Run Docker Push Script + # if: github.ref == 'refs/heads/main' + # run: bash docker-push.sh \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 39aaf97..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: go -sudo: false -# os: -# - linux -# - osx -services: -- docker -env: -- GO111MODULE=on -go: -- 1.12.x -before_script: -# - printf "$GITHUB_OAUTH_TOKEN" > .githubtoken -- make GOBIN=$HOME/gopath/bin deps -script: -- make GOBIN=$HOME/gopath/bin test -- make GOBIN=$HOME/gopath/bin build -- docker build -t clintsharp/gogen . -deploy: - - provider: s3 - region: us-west-1 - skip_cleanup: true - local_dir: build - access_key_id: AKIAIY55APUOQ5GBDPNA - secret_access_key: - secure: d39ztGaH4CwKQA2KVoIkeVlBu1SNjN6adL0M3AWfSraZJ3CfiixVZ1JFK046GdjUsvk0u+hjWj5NdywSkWJjCzbNK8hA7KBI5aBkFgKRuY3F6Ww7khBgJ3LZW9bVb05xD9JUvIcN1b/VqYth3uZuA1bde8VOUTzavbKLjDfbO+t8S9OlNZ2av7ZekAgx2pgZ9h1FLtellefmP0ro8QhBFTZJuJU++fc7ITIhRyjQMPmzce68ipd0I1cguOkPHk6uVFkTjSFfslQujNgEdfHnBbMAt+1MDk1WCkMcGyJc06zjnQthZhpgQGIJDma9t3elrKHzQo/zt16B/KJKHCtiY6VPB1D/MgxPCaf+ubW++6iTOmY+1TpOCT4E+AfvREx2SNbHnFf2yYsopW+R5IsES9rBH0vyLidJT4JItb5F+xWqeS1KZHH7SjBH7V86zyJTlCt4mljp/znTzGaJUdOH3ouXkRo32aSkNCCpST2gFSY3lzLlkG76gUsDUZh12MyAnOlNn8o+wXXi/9bcit8eyJQGnpERDQvwnOir49KOpAEYhWvuh8TFclcSNqSQeTHXNXLVlNXSMbH7vgsSlIpYsr5IJ2qdazVFYT6uLM/oJhLelLmTrcJ3iY8mZacSIYVQqQYQHWB0ZuvrceEcyvoY6LAb5asI3z2db4PkWLD/iZg= - bucket: gogen-artifacts - on: - repo: coccyx/gogen - - provider: script - script: bash docker-push.sh - - diff --git a/Makefile b/Makefile index 191d58e..c8543eb 100644 --- a/Makefile +++ b/Makefile @@ -20,16 +20,13 @@ endif all: install build: -# $(GOBIN)/roveralls -# $(GOBIN)/goveralls -coverprofile=roveralls.coverprofile -service=travis-ci -repotoken $$COVERALLS_TOKEN GOOS=linux CGO_ENABLED=0 GOARCH=amd64 go build -tags netgo $(FLAGS) -o build/linux/gogen GOOS=darwin GOARCH=amd64 go build $(FLAGS) -o build/osx/gogen GOOS=windows GOARCH=amd64 go build $(FLAGS) -o build/windows/gogen.exe GOOS=js GOARCH=wasm go build $(FLAGS) -o build/wasm/gogen.wasm deps: - go get -u github.com/mattn/goveralls - go get -u github.com/LawrenceWoodman/roveralls + go install github.com/mattn/goveralls@latest install: go install $(FLAGS) diff --git a/internal/config.go b/internal/config.go index 09d78e5..7e985e8 100644 --- a/internal/config.go +++ b/internal/config.go @@ -1268,3 +1268,43 @@ func (c *Config) Clean() { c.Samples = samples debug.FreeOSMemory() } + +// WriteFileFromString writes a configuration string to a temporary file and returns the filename +func WriteTempConfigFileFromString(config string) string { + tmpfile, err := os.CreateTemp("", "gogen-test-*.yml") + if err != nil { + panic(err) + } + + if _, err := tmpfile.Write([]byte(config)); err != nil { + tmpfile.Close() + panic(err) + } + if err := tmpfile.Close(); err != nil { + panic(err) + } + + return tmpfile.Name() +} + +func SetupFromFile(filename string) { + os.Setenv("GOGEN_HOME", "..") + os.Setenv("GOGEN_ALWAYS_REFRESH", "1") + os.Setenv("GOGEN_FULLCONFIG", filename) +} + +func SetupFromString(configStr string) { + configFile := WriteTempConfigFileFromString(configStr) + os.Setenv("GOGEN_HOME", "..") + os.Setenv("GOGEN_ALWAYS_REFRESH", "1") + os.Setenv("GOGEN_FULLCONFIG", configFile) +} + +func CleanupConfigAndEnvironment() { + os.Unsetenv("GOGEN_HOME") + os.Unsetenv("GOGEN_ALWAYS_REFRESH") + if strings.Contains(os.Getenv("GOGEN_FULLCONFIG"), "gogen-test-") { + os.Remove(os.Getenv("GOGEN_FULLCONFIG")) + } + os.Unsetenv("GOGEN_FULLCONFIG") +} diff --git a/internal/config_test.go b/internal/config_test.go index c9a7de5..8e565af 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -70,19 +70,55 @@ func TestFileOutput(t *testing.T) { } func TestHTTPOutput(t *testing.T) { - // Setup environment - os.Setenv("GOGEN_HOME", "..") - os.Setenv("GOGEN_ALWAYS_REFRESH", "1") - os.Setenv("GOGEN_FULLCONFIG", filepath.Join("..", "tests", "httpoutput", "httpoutput.yml")) - // os.Setenv("GOGEN_SAMPLES_DIR", filepath.Join(home, "config", "tests", "fileoutput.yml")) + configStr := ` +global: + output: + outputter: http + outputTemplate: json + endpoints: + - http://localhost:8088/http + headers: + Authorization: Splunk 00112233-4455-6677-8899-AABBCCDDEEFF +samples: + - name: outputhttpsample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: httptest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ +` + + SetupFromString(configStr) + c := NewConfig() headers := map[string]string{"Authorization": "Splunk 00112233-4455-6677-8899-AABBCCDDEEFF"} - endpoints := []string{"http://requestb.in/1hi5xoa1"} + endpoints := []string{"http://localhost:8088/http"} de := reflect.DeepEqual(headers, c.Global.Output.Headers) assert.True(t, de, "Headers do not match: %#v vs %#v", headers, c.Global.Output.Headers) de = reflect.DeepEqual(endpoints, c.Global.Output.Endpoints) assert.True(t, de, "Endpoints do not match: %#v vs %#v", endpoints, c.Global.Output.Endpoints) + + CleanupConfigAndEnvironment() } func TestFlatten(t *testing.T) { @@ -197,3 +233,126 @@ func FindSampleInFile(home string, name string) *Sample { // c.Log.Debugf("Pretty Values %# v\n", pretty.Formatter(c)) return c.FindSampleByName(name) } + +func TestWriteFileFromString(t *testing.T) { + testConfig := `name: test-config +description: test config file +disabled: false` + + filename := WriteTempConfigFileFromString(testConfig) + defer os.Remove(filename) // Clean up after test + + // Verify file exists + _, err := os.Stat(filename) + if err != nil { + t.Fatalf("Expected file %s to exist, got error: %v", filename, err) + } + + // Read contents and verify + contents, err := os.ReadFile(filename) + if err != nil { + t.Fatalf("Error reading file %s: %v", filename, err) + } + + if string(contents) != testConfig { + t.Errorf("File contents do not match. Expected:\n%s\nGot:\n%s", testConfig, string(contents)) + } +} +func TestSetupFromString(t *testing.T) { + testConfig := `name: test-setup +description: test setup config +disabled: false` + + // Run setup + SetupFromString(testConfig) + defer CleanupConfigAndEnvironment() // Clean up environment after test + + // Verify environment variables were set correctly + if os.Getenv("GOGEN_HOME") != ".." { + t.Errorf("Expected GOGEN_HOME to be '..', got '%s'", os.Getenv("GOGEN_HOME")) + } + + if os.Getenv("GOGEN_ALWAYS_REFRESH") != "1" { + t.Errorf("Expected GOGEN_ALWAYS_REFRESH to be '1', got '%s'", os.Getenv("GOGEN_ALWAYS_REFRESH")) + } + + // Verify config file was created and contains correct content + configFile := os.Getenv("GOGEN_FULLCONFIG") + if configFile == "" { + t.Fatal("Expected GOGEN_FULLCONFIG to be set") + } + + contents, err := os.ReadFile(configFile) + if err != nil { + t.Fatalf("Error reading config file: %v", err) + } + + if string(contents) != testConfig { + t.Errorf("Config file contents do not match. Expected:\n%s\nGot:\n%s", testConfig, string(contents)) + } +} + +func TestCleanup(t *testing.T) { + // Set up test environment variables + os.Setenv("GOGEN_HOME", "test-home") + os.Setenv("GOGEN_ALWAYS_REFRESH", "1") + + // Create a temporary config file + configFile := WriteTempConfigFileFromString("test config content") + os.Setenv("GOGEN_FULLCONFIG", configFile) + + // Run cleanup + CleanupConfigAndEnvironment() + + // Verify environment variables were unset + if val := os.Getenv("GOGEN_HOME"); val != "" { + t.Errorf("Expected GOGEN_HOME to be unset, got '%s'", val) + } + + if val := os.Getenv("GOGEN_ALWAYS_REFRESH"); val != "" { + t.Errorf("Expected GOGEN_ALWAYS_REFRESH to be unset, got '%s'", val) + } + + if val := os.Getenv("GOGEN_FULLCONFIG"); val != "" { + t.Errorf("Expected GOGEN_FULLCONFIG to be unset, got '%s'", val) + } + + // Verify config file was deleted + if _, err := os.Stat(configFile); !os.IsNotExist(err) { + t.Error("Expected config file to be deleted") + } +} + +func TestParseWebConfig(t *testing.T) { + // Set up test environment + os.Setenv("GOGEN_HOME", "..") + os.Setenv("GOGEN_ALWAYS_REFRESH", "1") + os.Setenv("GOGEN_FULLCONFIG", "https://gist.githubusercontent.com/coccyx/98d5b83307b0b85c1c7a54a08bfec8ed/raw/1ea26d1a16ffeeb113931e696e22b17f0eb0dc81/config.yaml") + defer CleanupConfigAndEnvironment() + + c := NewConfig() + // Validate global settings + assert.Equal(t, false, c.Global.Debug) + assert.Equal(t, 1, c.Global.GeneratorWorkers) + assert.Equal(t, 1, c.Global.OutputWorkers) + assert.Equal(t, "stdout", c.Global.Output.Outputter) + assert.Equal(t, "raw", c.Global.Output.OutputTemplate) + + // Validate sample configuration + assert.Equal(t, 1, len(c.Samples)) + sample := c.Samples[0] + assert.Equal(t, "weblog", sample.Name) + assert.Equal(t, 10, sample.Count) + assert.Equal(t, 1, sample.Interval) + assert.Equal(t, "now", sample.Earliest) + assert.Equal(t, "now", sample.Latest) + assert.Equal(t, true, sample.RandomizeEvents) + assert.Equal(t, "sample", sample.Generator) + + // Validate tokens + assert.Equal(t, 8, len(sample.Tokens)) + tsToken := sample.Tokens[0] + assert.Equal(t, "ts-dmyhmsms-template", tsToken.Name) + assert.Equal(t, "timestamp", tsToken.Type) + assert.Equal(t, "%d/%b/%Y %H:%M:%S:%L", tsToken.Replacement) +} diff --git a/logger/logger.go b/logger/logger.go index 17e92e5..c713fb8 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -31,14 +31,14 @@ func (hook ContextHook) Levels() []logrus.Level { // Fire is a callback issued for every log message, allowing us to modify the output, required for Logrus Hook implementation func (hook ContextHook) Fire(entry *logrus.Entry) error { - pc := make([]uintptr, 5, 5) + pc := make([]uintptr, 5) cnt := runtime.Callers(6, pc) for i := 0; i < cnt; i++ { fu := runtime.FuncForPC(pc[i] - 2) name := fu.Name() if !strings.Contains(name, "github.com/sirupsen/logrus") && - !strings.Contains(name, "github.com/coccyx/gogen/logger") { + !(strings.Contains(name, "github.com/coccyx/gogen/logger") && !strings.Contains(name, "TestContextHook")) { file, line := fu.FileLine(pc[i] - 2) entry.Data["file"] = path.Base(file) entry.Data["func"] = path.Base(name) diff --git a/logger/logger_test.go b/logger/logger_test.go index 5297cb2..e2a9dc5 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -1,13 +1,303 @@ package logging import ( + "bytes" + "encoding/json" + "errors" + "os" + "strings" "testing" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) +// captureOutput is an internal test helper that temporarily captures log output +func captureOutput(f func()) string { + var buf bytes.Buffer + original := logrus.StandardLogger().Out + logrus.SetOutput(&buf) + f() + logrus.SetOutput(original) + return buf.String() +} + func TestLogLevel(t *testing.T) { - if DefaultLogLevel != logrus.ErrorLevel { - t.Fatalf("Log not set to Error") + // Test default log level + assert.Equal(t, logrus.ErrorLevel, DefaultLogLevel, "Default log level should be Error") + + // Test setting different log levels and verify through log visibility + tests := []struct { + name string + setLevel func() + shouldLogInfo bool // Info messages should be visible at Info level and below + }{ + {"Debug", func() { SetDebug(true) }, true}, + {"Info", func() { SetInfo() }, true}, + {"Warn", func() { SetWarn() }, false}, + {"Debug Off", func() { SetDebug(false) }, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setLevel() + output := captureOutput(func() { + Info("test message") + }) + if tt.shouldLogInfo { + assert.Contains(t, output, "test message") + } else { + assert.Empty(t, output) + } + }) + } +} + +func TestLoggingMethods(t *testing.T) { + SetDebug(true) // Set to debug to capture all messages + defer SetDebug(false) + + tests := []struct { + name string + logFunc func() + contains string + level string + }{ + { + name: "Debug", + logFunc: func() { Debug("debug message") }, + contains: "debug message", + level: "debug", + }, + { + name: "Info", + logFunc: func() { Info("info message") }, + contains: "info message", + level: "info", + }, + { + name: "Warning", + logFunc: func() { Warning("warning message") }, + contains: "warning message", + level: "warning", + }, + { + name: "Error", + logFunc: func() { Error("error message") }, + contains: "error message", + level: "error", + }, + { + name: "Debugf", + logFunc: func() { Debugf("debug %s", "formatted") }, + contains: "debug formatted", + level: "debug", + }, + { + name: "Infof", + logFunc: func() { Infof("info %s", "formatted") }, + contains: "info formatted", + level: "info", + }, + { + name: "Warningf", + logFunc: func() { Warningf("warning %s", "formatted") }, + contains: "warning formatted", + level: "warning", + }, + { + name: "Errorf", + logFunc: func() { Errorf("error %s", "formatted") }, + contains: "error formatted", + level: "error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := captureOutput(tt.logFunc) + if output == "" { + t.Logf("Warning: Empty output received for test case %s", tt.name) + } + assert.Contains(t, strings.ToLower(output), tt.contains) + assert.Contains(t, strings.ToLower(output), tt.level) + }) + } +} + +func TestLogLevelFiltering(t *testing.T) { + // Test Info level filtering + SetInfo() + defer SetDebug(false) + + // Debug messages should not appear at Info level + debugOutput := captureOutput(func() { + Debug("debug message") + }) + assert.Empty(t, debugOutput, "Debug message should not appear at Info level") + + // Info messages should appear + infoOutput := captureOutput(func() { + Info("info message") + }) + assert.Contains(t, infoOutput, "info message") +} + +func TestWithField(t *testing.T) { + SetInfo() + defer SetDebug(false) + + output := captureOutput(func() { + WithField("single_key", "single_value").Info("single field test") + }) + + assert.Contains(t, output, "single_key") + assert.Contains(t, output, "single_value") +} + +func TestFatal(t *testing.T) { + // Create a channel to signal if os.Exit was called + executed := make(chan bool, 1) + + // Save original os.Exit + originalExit := logrus.StandardLogger().ExitFunc + defer func() { + logrus.StandardLogger().ExitFunc = originalExit + }() + + // Override os.Exit + logrus.StandardLogger().ExitFunc = func(code int) { + executed <- true + } + + output := captureOutput(func() { + Fatal("fatal message") + }) + + select { + case <-executed: + assert.Contains(t, output, "fatal message") + default: + t.Error("Fatal did not trigger exit") + } + + // Test Fatalf + output = captureOutput(func() { + Fatalf("fatal %s", "formatted") + }) + + select { + case <-executed: + assert.Contains(t, output, "fatal formatted") + default: + t.Error("Fatalf did not trigger exit") + } +} + +func TestWithFields(t *testing.T) { + SetInfo() + defer SetDebug(false) + + fields := Fields{ + "key1": "value1", + "key2": 42, } + + output := captureOutput(func() { + WithFields(fields).Info("test message") + }) + + assert.Contains(t, output, "key1") + assert.Contains(t, output, "value1") + assert.Contains(t, output, "key2") + assert.Contains(t, output, "42") +} + +func TestWithError(t *testing.T) { + SetInfo() + defer SetDebug(false) + + testErr := errors.New("test error") + + output := captureOutput(func() { + WithError(testErr).Error("error occurred") + }) + + assert.Contains(t, output, "test error") + assert.Contains(t, output, "error occurred") +} + +func TestJSONOutput(t *testing.T) { + EnableJSONOutput() + defer EnableTextOutput() + SetInfo() + defer SetDebug(false) + + output := captureOutput(func() { + WithField("test_field", "test_value").Info("test message") + }) + + t.Logf("JSON Output received: %s", output) + + var logEntry map[string]interface{} + err := json.Unmarshal([]byte(strings.TrimSpace(output)), &logEntry) + assert.NoError(t, err) + assert.Equal(t, "test_value", logEntry["test_field"]) + assert.Equal(t, "test message", logEntry["msg"]) +} + +func TestFileOutput(t *testing.T) { + testFile := "test.log" + SetOutput(testFile) + SetInfo() + defer SetDebug(false) + + Info("test file output") + + // Read the file content + content, err := os.ReadFile(testFile) + assert.NoError(t, err) + assert.Contains(t, string(content), "test file output") + + // Cleanup + os.Remove(testFile) +} + +func TestContextHook(t *testing.T) { + EnableJSONOutput() + defer EnableTextOutput() + SetDebug(true) + defer SetDebug(false) + + output := captureOutput(func() { + Info("test context hook") + }) + + t.Logf("Output received: %s", output) + + var logEntry map[string]interface{} + err := json.Unmarshal([]byte(strings.TrimSpace(output)), &logEntry) + if assert.NoError(t, err) { + assert.NotEmpty(t, logEntry["file"], "file field should be present") + assert.NotEmpty(t, logEntry["func"], "func field should be present") + assert.NotNil(t, logEntry["line"], "line field should be present") + + t.Logf("File: %v", logEntry["file"]) + t.Logf("Func: %v", logEntry["func"]) + t.Logf("Line: %v", logEntry["line"]) + } +} + +func TestPanicRecovery(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("The code did not panic as expected") + } + }() + + output := captureOutput(func() { + Panic("test panic") + }) + + assert.Contains(t, output, "test panic") } diff --git a/run/runOnce.go b/run/runOnce.go index a356076..39be3c7 100644 --- a/run/runOnce.go +++ b/run/runOnce.go @@ -17,6 +17,11 @@ type Runner struct{} func (r Runner) Once(name string) { c := config.NewConfig() go outputter.ROT(c) + r.onceWithConfig(name, c) +} + +// onceWithConfig runs a sample once using the provided config +func (r Runner) onceWithConfig(name string, c *config.Config) { s := c.FindSampleByName(name) source := rand.NewSource(time.Now().UnixNano()) @@ -26,7 +31,7 @@ func (r Runner) Once(name string) { log.Fatalf("Description not set for sample '%s'", s.Name) } - log.Debugf("Generating for Push() sample '%s'", s.Name) + log.Debugf("Generating for sample '%s'", s.Name) origOutputter := s.Output.Outputter origOutputTemplate := s.Output.OutputTemplate s.Output.Outputter = "buf" @@ -36,17 +41,34 @@ func (r Runner) Once(name string) { oq := make(chan *config.OutQueueItem) oqs := make(chan int) - go generator.Start(gq, gqs) + // Start outputter first so it's ready to receive go outputter.Start(oq, oqs, 1) + // Then start generator + go generator.Start(gq, gqs) - gqi := &config.GenQueueItem{Count: 1, Earliest: time.Now(), Latest: time.Now(), S: s, OQ: oq, Rand: randgen, Event: -1, Cache: &config.CacheItem{ - UseCache: false, - SetCache: false, - }} - gq <- gqi + // Get current time for event generation + now := time.Now() + if c.Global.UTC { + now = now.UTC() + } - time.Sleep(time.Second) + // Send generation request + gqi := &config.GenQueueItem{ + Count: 1, + Earliest: now, + Latest: now, + S: s, + OQ: oq, + Rand: randgen, + Event: -1, + Cache: &config.CacheItem{ + UseCache: false, + SetCache: false, + }, + } + gq <- gqi + // Close generator and wait for it to finish log.Debugf("Closing generator channel") close(gq) @@ -56,9 +78,16 @@ Loop1: case <-gqs: log.Debugf("Generator closed") break Loop1 + case <-time.After(2 * time.Second): + log.Debugf("Generator timeout waiting for close signal") + break Loop1 } } + // Give outputter time to process any remaining items + time.Sleep(100 * time.Millisecond) + + // Now close outputter and wait for it to finish log.Debugf("Closing outputter channel") close(oq) @@ -68,11 +97,14 @@ Loop2: case <-oqs: log.Debugf("Outputter closed") break Loop2 + case <-time.After(2 * time.Second): + log.Debugf("Outputter timeout waiting for close signal") + break Loop2 } } s.Output.Outputter = origOutputter s.Output.OutputTemplate = origOutputTemplate - log.Debugf("Buffer: %s", c.Buf.String()) + log.Debugf("Buffer contents: %s", s.Buf.String()) } diff --git a/run/runonce_test.go b/run/runonce_test.go new file mode 100644 index 0000000..e40ec2d --- /dev/null +++ b/run/runonce_test.go @@ -0,0 +1,126 @@ +package run + +import ( + "encoding/json" + "strconv" + "testing" + "time" + + config "github.com/coccyx/gogen/internal" + "github.com/stretchr/testify/assert" +) + +func TestOnceWithConfig(t *testing.T) { + // Clean up any existing config + config.ResetConfig() + + // Setup test configuration + configStr := ` +global: + debug: true + verbose: true + utc: true + output: + outputter: buf + outputTemplate: json +samples: + - name: runoncesample + description: "Test sample for runOnce functionality" + begin: now + end: now + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S.%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: runonce_test + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ + field1: test1 + field2: test2 +` + + // Setup config and get the instance that will be used + config.SetupFromString(configStr) + c := config.NewConfig() + + // Record time before and after test to validate timestamp is within range + beforeTest := time.Now().Truncate(time.Second) + if c.Global.UTC { + beforeTest = beforeTest.UTC() + } + + // Create a new Runner and run the sample once using the private method + runner := Runner{} + runner.onceWithConfig("runoncesample", c) + + afterTest := time.Now().Truncate(time.Second) + if c.Global.UTC { + afterTest = afterTest.UTC() + } + + // Get the sample and its buffer + output := c.Buf.String() + t.Logf("Buffer contents: %s", output) + + // Parse the JSON output + var jsonData map[string]interface{} + err := json.Unmarshal([]byte(output), &jsonData) + assert.NoError(t, err, "Failed to parse JSON output") + + // Validate the expected fields + expectedFields := []string{"sourcetype", "source", "host", "index", "_time", "_raw", "field1", "field2"} + for _, field := range expectedFields { + assert.Contains(t, jsonData, field, "Missing expected field %s in JSON output", field) + } + + // Validate specific field values + assert.Equal(t, "runonce_test", jsonData["sourcetype"]) + assert.Equal(t, "gogen", jsonData["source"]) + assert.Equal(t, "gogen", jsonData["host"]) + assert.Equal(t, "main", jsonData["index"]) + assert.Equal(t, "test1", jsonData["field1"]) + assert.Equal(t, "test2", jsonData["field2"]) + + // Check _time is within the test execution window + eventEpoch, err := strconv.ParseFloat(jsonData["_time"].(string), 64) + assert.NoError(t, err, "Failed to parse _time as float") + // Truncate to seconds for comparison + eventTime := time.Unix(int64(eventEpoch), 0).Truncate(time.Second) + if c.Global.UTC { + eventTime = eventTime.UTC() + } + + // The event time should be equal to either beforeTest or afterTest + // since we truncated all times to seconds + assert.True(t, eventTime.Equal(beforeTest) || eventTime.Equal(afterTest), + "Event time %v should equal either test start time %v or end time %v", + eventTime, beforeTest, afterTest) + + // Parse and check _raw timestamp format + rawTime, err := time.Parse("02/Jan/2006 15:04:05.000", jsonData["_raw"].(string)) + assert.NoError(t, err, "Failed to parse _raw timestamp") + if c.Global.UTC { + rawTime = rawTime.UTC() + } + rawTime = rawTime.Truncate(time.Second) + assert.True(t, rawTime.Equal(eventTime), + "Raw timestamp %v should match event time %v", rawTime, eventTime) + + // Clean up after test + config.CleanupConfigAndEnvironment() +} diff --git a/template/template_test.go b/template/template_test.go index ac7297c..2e53773 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -10,36 +10,68 @@ func TestTemplate(t *testing.T) { row := map[string]string{"_raw": "foo", "index": "fooindex", "host": "barhost"} // Try to call Exec first, should error - temp, err := Exec("test", row) + _, err := Exec("test", row) assert.EqualError(t, err, "Exec called for template 'test' but not found in cache") // Create a new test template err = New("test", "{{ ._raw }}") - temp, err = Exec("test", row) + assert.NoError(t, err) + temp, err := Exec("test", row) + assert.NoError(t, err) assert.Equal(t, "foo", temp) // More complicated err = New("test2", "index={{ .index}} host={{ .host }} _raw={{ ._raw }}") + assert.NoError(t, err) temp, err = Exec("test2", row) + assert.NoError(t, err) assert.Equal(t, "index=fooindex host=barhost _raw=foo", temp) // JSON err = New("test3", "{{ json . | printf \"%s\" }}") + assert.NoError(t, err) temp, err = Exec("test3", row) + assert.NoError(t, err) assert.Equal(t, `{"_raw":"foo","host":"barhost","index":"fooindex"}`, temp) // Multiple variables, one replacement err = New("test4", "{{ ._raw }}{{ .foo }}") + assert.NoError(t, err) temp, err = Exec("test4", row) + assert.NoError(t, err) assert.Equal(t, "foo", temp) err = New("testheader", `{{ keys . | join "," }}`) + assert.NoError(t, err) temp, err = Exec("testheader", row) + assert.NoError(t, err) assert.Equal(t, "_raw,host,index", temp) // fmt.Println(temp) err = New("testvalues", `{{ values . | join "," }}`) + assert.NoError(t, err) temp, err = Exec("testvalues", row) + assert.NoError(t, err) assert.Equal(t, "foo,barhost,fooindex", temp) // fmt.Println(temp) + + // Test splunkhec template + row = map[string]string{"_raw": "test raw", "_time": "1234567890.123", "host": "testhost", "source": "testsource"} + err = New("splunkhec", "{{ splunkhec . }}") + assert.NoError(t, err) + temp, err = Exec("splunkhec", row) + assert.NoError(t, err) + assert.Equal(t, `{"event":"test raw","host":"testhost","source":"testsource","time":"1234567890.123"}`, temp) +} + +func TestExists(t *testing.T) { + // Test non-existent template + exists := Exists("nonexistent") + assert.False(t, exists, "Template 'nonexistent' should not exist") + + // Create a new template and verify it exists + err := New("testexists", "test template") + assert.NoError(t, err) + exists = Exists("testexists") + assert.True(t, exists, "Template 'testexists' should exist") } diff --git a/tests/devnull_test.go b/tests/devnull_test.go new file mode 100644 index 0000000..ed8d3bb --- /dev/null +++ b/tests/devnull_test.go @@ -0,0 +1,120 @@ +package tests + +import ( + "encoding/json" + "os" + "strings" + "testing" + "time" + + config "github.com/coccyx/gogen/internal" + log "github.com/coccyx/gogen/logger" + "github.com/coccyx/gogen/run" + "github.com/stretchr/testify/assert" +) + +func TestDevNullOutput(t *testing.T) { + // Create a temporary log file + tmpFile, err := os.CreateTemp("", "gogen_devnull_test_*.log") + assert.NoError(t, err) + logFile := tmpFile.Name() + tmpFile.Close() // Close it so logger can open it + + // Set up logging to the temp file + log.SetOutput(logFile) + log.EnableJSONOutput() + log.SetInfo() + + // Clean up logging at the end + defer func() { + log.EnableTextOutput() + log.SetDebug(false) + os.Remove(logFile) + }() + + configStr := ` +global: + debug: true + verbose: true + output: + outputter: devnull + outputTemplate: json +samples: + - name: devnullsample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: devnulltest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + // Run the sample + run.Run(c) + + // Wait a bit for the readout thread to complete + time.Sleep(2 * time.Second) + + // Read the log file contents + logData, err := os.ReadFile(logFile) + assert.NoError(t, err) + logOutput := string(logData) + + // Verify that log messages were written correctly + assert.Contains(t, logOutput, "Setting outputter") + assert.Contains(t, logOutput, "devnull") + + // Check for eventSec and kbytesSec > 0 in the JSON logs + foundEventSec := false + foundKbytesSec := false + for _, line := range strings.Split(logOutput, "\n") { + if line == "" { + continue + } + var logEntry map[string]interface{} + err := json.Unmarshal([]byte(line), &logEntry) + assert.NoError(t, err) + + if rate, ok := logEntry["eventsSec"]; ok { + if rateFloat, ok := rate.(float64); ok && rateFloat > 0 { + foundEventSec = true + } + } + if rate, ok := logEntry["kbytesSec"]; ok { + if rateFloat, ok := rate.(float64); ok && rateFloat > 0 { + foundKbytesSec = true + } + } + if foundEventSec && foundKbytesSec { + break + } + } + assert.True(t, foundEventSec, "Expected to find eventSec > 0 in logs") + assert.True(t, foundKbytesSec, "Expected to find kbytesSec > 0 in logs") + + // Verify that no output was written to the buffer + assert.Empty(t, c.Buf.String(), "Expected no output in buffer for devnull outputter") + + config.CleanupConfigAndEnvironment() +} diff --git a/tests/file_test.go b/tests/file_test.go index 579cef7..8db560a 100644 --- a/tests/file_test.go +++ b/tests/file_test.go @@ -12,11 +12,7 @@ import ( ) func TestFileOutput(t *testing.T) { - // Setup environment - os.Setenv("GOGEN_HOME", "..") - os.Setenv("GOGEN_ALWAYS_REFRESH", "1") - home := ".." - os.Setenv("GOGEN_FULLCONFIG", filepath.Join(home, "tests", "fileoutput", "fileoutput.yml")) + config.SetupFromFile(filepath.Join("..", "tests", "fileoutput", "fileoutput.yml")) c := config.NewConfig() // s := c.FindSampleByName("backfill") run.Run(c) @@ -24,19 +20,13 @@ func TestFileOutput(t *testing.T) { info, err := os.Stat(c.Global.Output.FileName) assert.NoError(t, err) assert.Condition(t, func() bool { - if info.Size() < c.Global.Output.MaxBytes { - return true - } - return false + return info.Size() < c.Global.Output.MaxBytes }, "Rotation failing, main file size of %d greater than MaxBytes %d", info.Size(), c.Global.Output.MaxBytes) for i := 1; i <= c.Global.Output.BackupFiles; i++ { info, err = os.Stat(c.Global.Output.FileName + "." + strconv.Itoa(i)) assert.NoError(t, err) assert.Condition(t, func() bool { - if info.Size() > c.Global.Output.MaxBytes { - return true - } - return false + return info.Size() > c.Global.Output.MaxBytes }, "Rotation failing, file %d less than MaxBytes", i) } diff --git a/tests/http_test.go b/tests/http_test.go index 30873f8..7991788 100644 --- a/tests/http_test.go +++ b/tests/http_test.go @@ -1,31 +1,304 @@ package tests import ( - "os" - "path/filepath" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strings" "testing" + "time" config "github.com/coccyx/gogen/internal" "github.com/coccyx/gogen/run" ) +var lastRequest []byte + +func setupTestHTTPServer(endpoint string) *http.Server { + server := &http.Server{Addr: ":8088"} + + http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + panic(err) + } + lastRequest = body + w.WriteHeader(http.StatusOK) + }) + + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + panic(err) + } + }() + + // Give the server a moment to start up + time.Sleep(100 * time.Millisecond) + + return server +} + func TestHTTPOutput(t *testing.T) { - // Setup environment - os.Setenv("GOGEN_HOME", "..") - os.Setenv("GOGEN_ALWAYS_REFRESH", "1") - home := ".." - os.Setenv("GOGEN_FULLCONFIG", filepath.Join(home, "tests", "httpoutput", "httpoutput.yml")) + server := setupTestHTTPServer("/http") + defer server.Close() + + configStr := ` +global: + output: + outputter: http + outputTemplate: json + endpoints: + - http://localhost:8088/http + headers: + Authorization: Splunk 00112233-4455-6677-8899-AABBCCDDEEFF +samples: + - name: outputhttpsample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: httptest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + run.Run(c) + + // Verify the last request was received and formatted correctly + if len(lastRequest) == 0 { + t.Fatal("No request received") + } + + // Parse the JSON + var jsonData map[string]interface{} + err := json.Unmarshal([]byte(lastRequest), &jsonData) + if err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + // Validate the expected fields exist + expectedFields := []string{"sourcetype", "source", "host", "index", "_time", "_raw"} + for _, field := range expectedFields { + if _, ok := jsonData[field]; !ok { + t.Errorf("Missing expected field %s in JSON output", field) + } + } + + // Validate specific field values + if jsonData["sourcetype"] != "httptest" || + jsonData["source"] != "gogen" || + jsonData["host"] != "gogen" || + jsonData["index"] != "main" { + t.Error("Basic fields don't match expected values") + } + + // Check _time is correct epoch for 2001-10-20 00:00:00 + expectedEpoch := fmt.Sprintf("%.3f", float64(time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local).Unix())) + if jsonData["_time"] != expectedEpoch { + t.Errorf("Expected _time to be %s, got %v", expectedEpoch, jsonData["_time"]) + } + + // Check _raw has correct timestamp format + expectedRaw := "20/Oct/2001 00:00:00:000" + if jsonData["_raw"] != expectedRaw { + t.Errorf("Expected _raw to be %s, got %v", expectedRaw, jsonData["_raw"]) + } + + config.CleanupConfigAndEnvironment() +} + +func TestHTTPSplunkOutput(t *testing.T) { + server := setupTestHTTPServer("/splunk") + defer server.Close() + + configStr := ` +global: + output: + outputter: http + outputTemplate: splunkhec + endpoints: + - http://localhost:8088/splunk + headers: + Authorization: Splunk 00112233-4455-6677-8899-AABBCCDDEEFF +samples: + - name: outputhttpsample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + + lines: + - sourcetype: httptest + source: gogen + host: gogen + index: main + _raw: $ts$ +` + + config.SetupFromString(configStr) c := config.NewConfig() - if len(c.Samples) > 0 { - // s := c.FindSampleByName("backfill") - run.Run(c) - // open.Run(c.Global.Output.Endpoints[0] + "?inspect") + + run.Run(c) + + if len(lastRequest) == 0 { + t.Fatal("No request received") } - os.Setenv("GOGEN_FULLCONFIG", filepath.Join(home, "tests", "httpoutput", "splunkoutput.yml")) - c = config.NewConfig() - s := c.FindSampleByName("outputsample") - if s != nil { - run.Run(c) + // Parse the JSON + var jsonData map[string]interface{} + err := json.Unmarshal(lastRequest, &jsonData) + if err != nil { + t.Fatalf("Failed to parse JSON: %v", err) } + + // Validate expected fields for Splunk HEC format + expectedFields := map[string]string{ + "event": "20/Oct/2001 00:00:00:000", + "host": "gogen", + "index": "main", + "source": "gogen", + "sourcetype": "httptest", + "time": fmt.Sprintf("%.0f", float64(time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local).Unix())), + } + + for field, expected := range expectedFields { + actual, ok := jsonData[field] + if !ok { + t.Errorf("Missing expected field %s in HEC output", field) + continue + } + if actual != expected { + t.Errorf("Field %s: expected %q, got %q", field, expected, actual) + } + } + + config.CleanupConfigAndEnvironment() +} + +func TestHTTPElasticOutput(t *testing.T) { + server := setupTestHTTPServer("/elastic") + defer server.Close() + + configStr := ` +global: + output: + outputter: http + outputTemplate: elasticsearch + endpoints: + - http://localhost:8088/elastic + headers: + Authorization: Elastic 00112233-4455-6677-8899-AABBCCDDEEFF +samples: + - name: outputhttpsample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + + lines: + - sourcetype: httptest + source: gogen + host: gogen + index: main + _raw: $ts$ +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + run.Run(c) + + if len(lastRequest) == 0 { + t.Fatal("No request received") + } + + var jsonData map[string]interface{} + + // Split the request into lines for elastic format + lines := strings.Split(string(lastRequest), "\n") + if len(lines) != 3 || lines[2] != "" { + t.Fatal("Expected 2 lines for elastic format (header and data)") + } + + // Parse and validate the header line + var headerData map[string]map[string]string + if err := json.Unmarshal([]byte(lines[0]), &headerData); err != nil { + t.Fatalf("Failed to parse header JSON: %v", err) + } + + expectedHeader := map[string]map[string]string{ + "index": { + "_index": "main", + "_type": "doc", + }, + } + + if !reflect.DeepEqual(headerData, expectedHeader) { + t.Errorf("Header mismatch.\nExpected: %v\nGot: %v", expectedHeader, headerData) + } + + // Parse and validate the data line + if err := json.Unmarshal([]byte(lines[1]), &jsonData); err != nil { + t.Fatalf("Failed to parse data JSON: %v", err) + } + + // Create expected timestamp in the same timezone format that the code uses + testTime := time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local) + expectedTimestamp := testTime.Format(time.RFC3339) + + expectedFields := map[string]string{ + "@timestamp": expectedTimestamp, + "host": "gogen", + "index": "main", + "message": "20/Oct/2001 00:00:00:000", + "source": "gogen", + "sourcetype": "httptest", + } + + for field, expected := range expectedFields { + actual, ok := jsonData[field] + if !ok { + t.Errorf("Missing expected field %s in elastic output", field) + continue + } + if actual != expected { + t.Errorf("Field %s: expected %q, got %q", field, expected, actual) + } + } + + config.CleanupConfigAndEnvironment() } diff --git a/tests/httpoutput/httpoutput.yml b/tests/httpoutput/httpoutput.yml deleted file mode 100644 index 0cb642f..0000000 --- a/tests/httpoutput/httpoutput.yml +++ /dev/null @@ -1,56 +0,0 @@ -global: - debug: false - verbose: false - generatorWorkers: 1 - outputWorkers: 1 - rotInterval: 1 - output: - outputter: http - outputTemplate: json - endpoints: - - http://requestb.in/1hi5xoa1 - headers: - Authorization: Splunk 00112233-4455-6677-8899-AABBCCDDEEFF -samples: - - name: outputhttpsample - disabled: true - endIntervals: 1 - interval: 1 - count: 1 - tokens: - - name: ts-dmyhmsms-template - format: template - token: $ts$ - type: timestamp - replacement: "%d/%b/%Y %H:%M:%S:%L" - - name: tsepoch - format: template - token: $epochts$ - field: _time - type: timestamp - replacement: "%s.%L" - - name: transtype - format: template - type: weightedChoice - weightedChoice: - - weight: 3 - choice: New - - weight: 5 - choice: Change - - weight: 1 - choice: Delete - - name: value - format: template - type: random - replacement: float - precision: 3 - lower: 0 - upper: 10 - - lines: - - sourcetype: httptest - source: gogen - host: gogen - index: main - _time: $epochts$ - _raw: $ts$ transtype=$transtype$ value=$value$ \ No newline at end of file diff --git a/tests/httpoutput/splunkoutput.yml b/tests/httpoutput/splunkoutput.yml deleted file mode 100644 index 5627423..0000000 --- a/tests/httpoutput/splunkoutput.yml +++ /dev/null @@ -1,56 +0,0 @@ -global: - debug: false - verbose: false - generatorWorkers: 1 - outputWorkers: 1 - rotInterval: 1 - output: - outputter: http - outputTemplate: splunkhec - endpoints: - - http://localhost:8088/services/collector/event - headers: - Authorization: Splunk 00112233-4455-6677-8899-AABBCCDDEEFF -samples: - - name: outputsample - disabled: true - endIntervals: 1 - interval: 1 - count: 1 - tokens: - - name: ts-dmyhmsms-template - format: template - token: $ts$ - type: timestamp - replacement: "%d/%b/%Y %H:%M:%S:%L" - - name: tsepoch - format: template - token: $epochts$ - field: _time - type: timestamp - replacement: "%s.%L" - - name: transtype - format: template - type: weightedChoice - weightedChoice: - - weight: 3 - choice: New - - weight: 5 - choice: Change - - weight: 1 - choice: Delete - - name: value - format: template - type: random - replacement: float - precision: 3 - lower: 0 - upper: 10 - - lines: - - sourcetype: httptest - source: gogen - host: gogen - index: main - _time: $epochts$ - _raw: $ts$ transtype=$transtype$ value=$value$ \ No newline at end of file diff --git a/tests/network_test.go b/tests/network_test.go new file mode 100644 index 0000000..0bef478 --- /dev/null +++ b/tests/network_test.go @@ -0,0 +1,441 @@ +package tests + +import ( + "encoding/json" + "fmt" + "net" + "regexp" + "strings" + "testing" + "time" + + config "github.com/coccyx/gogen/internal" + "github.com/coccyx/gogen/run" +) + +var lastNetworkData []byte + +func setupTestTCPServer(port string) (*net.TCPListener, chan bool) { + addr, err := net.ResolveTCPAddr("tcp", ":"+port) + if err != nil { + panic(err) + } + + listener, err := net.ListenTCP("tcp", addr) + if err != nil { + panic(err) + } + + done := make(chan bool) + go func() { + conn, err := listener.Accept() + if err != nil { + panic(err) + } + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil { + panic(err) + } + lastNetworkData = buf[:n] + done <- true + }() + + return listener, done +} + +func setupTestUDPServer(port string) (*net.UDPConn, chan bool) { + addr, err := net.ResolveUDPAddr("udp", ":"+port) + if err != nil { + panic(err) + } + + conn, err := net.ListenUDP("udp", addr) + if err != nil { + panic(err) + } + + done := make(chan bool) + go func() { + buf := make([]byte, 1024) + n, _, err := conn.ReadFromUDP(buf) + if err != nil { + panic(err) + } + lastNetworkData = buf[:n] + done <- true + }() + + return conn, done +} + +func TestTCPOutput(t *testing.T) { + listener, done := setupTestTCPServer("8089") + defer listener.Close() + + configStr := ` +global: + output: + outputter: network + protocol: tcp + outputTemplate: json + endpoints: + - localhost:8089 +samples: + - name: outputnetworksample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: networktest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + run.Run(c) + + // Wait for the server to receive data + <-done + + // Verify the data was received + if len(lastNetworkData) == 0 { + t.Fatal("No data received over TCP") + } + + // Parse the JSON + var jsonData map[string]interface{} + err := json.Unmarshal(lastNetworkData, &jsonData) + if err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + // Validate the expected fields exist and have correct values + expectedFields := map[string]string{ + "sourcetype": "networktest", + "source": "gogen", + "host": "gogen", + "index": "main", + } + + for field, expected := range expectedFields { + actual, ok := jsonData[field] + if !ok { + t.Errorf("Missing expected field %s in TCP output", field) + continue + } + if actual != expected { + t.Errorf("Field %s: expected %q, got %q", field, expected, actual) + } + } + + // Check _time is correct epoch for 2001-10-20 00:00:00 + expectedEpoch := fmt.Sprintf("%.3f", float64(time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local).Unix())) + if jsonData["_time"] != expectedEpoch { + t.Errorf("Expected _time to be %s, got %v", expectedEpoch, jsonData["_time"]) + } + + // Check _raw has correct timestamp format + expectedRaw := "20/Oct/2001 00:00:00:000" + if jsonData["_raw"] != expectedRaw { + t.Errorf("Expected _raw to be %s, got %v", expectedRaw, jsonData["_raw"]) + } + + config.CleanupConfigAndEnvironment() +} + +func TestUDPOutput(t *testing.T) { + conn, done := setupTestUDPServer("8090") + defer conn.Close() + + configStr := ` +global: + output: + outputter: network + protocol: udp + outputTemplate: json + endpoints: + - localhost:8090 +samples: + - name: outputnetworksample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: networktest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + run.Run(c) + + // Wait for the server to receive data + <-done + + // Verify the data was received + if len(lastNetworkData) == 0 { + t.Fatal("No data received over UDP") + } + + // Parse the JSON + var jsonData map[string]interface{} + err := json.Unmarshal(lastNetworkData, &jsonData) + if err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + // Validate the expected fields exist and have correct values + expectedFields := map[string]string{ + "sourcetype": "networktest", + "source": "gogen", + "host": "gogen", + "index": "main", + } + + for field, expected := range expectedFields { + actual, ok := jsonData[field] + if !ok { + t.Errorf("Missing expected field %s in UDP output", field) + continue + } + if actual != expected { + t.Errorf("Field %s: expected %q, got %q", field, expected, actual) + } + } + + // Check _time is correct epoch for 2001-10-20 00:00:00 + expectedEpoch := fmt.Sprintf("%.3f", float64(time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local).Unix())) + if jsonData["_time"] != expectedEpoch { + t.Errorf("Expected _time to be %s, got %v", expectedEpoch, jsonData["_time"]) + } + + // Check _raw has correct timestamp format + expectedRaw := "20/Oct/2001 00:00:00:000" + if jsonData["_raw"] != expectedRaw { + t.Errorf("Expected _raw to be %s, got %v", expectedRaw, jsonData["_raw"]) + } + + config.CleanupConfigAndEnvironment() +} + +func TestTCPRFC3164Output(t *testing.T) { + listener, done := setupTestTCPServer("8091") + defer listener.Close() + + configStr := ` +global: + output: + outputter: network + protocol: tcp + outputTemplate: rfc3164 + endpoints: + - localhost:8091 +samples: + - name: outputnetworksample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + + lines: + - sourcetype: networktest + source: gogen + host: gogen + index: main + priority: "14" + tag: "gogen" + pid: "12345" + _raw: test message +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + run.Run(c) + + // Wait for the server to receive data + <-done + + // Verify the data was received + if len(lastNetworkData) == 0 { + t.Fatal("No data received over TCP") + } + + // Get expected timestamp in local time + expectedTime := time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local) + expectedTimeStr := expectedTime.Format("Jan 2 15:04:05") + + // RFC3164 format: timestamp hostname tag[pid]: message + // Actual format from output: <14>Oct 20 00:00:00 gogen gogen[12345]: test message + rfc3164Regex := regexp.MustCompile(`^<14>(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+gogen\s+gogen\[12345\]:\s+test message$`) + + if !rfc3164Regex.Match(lastNetworkData) { + t.Errorf("RFC3164 format mismatch. Got: %s", string(lastNetworkData)) + } + + // Extract and validate the timestamp + matches := rfc3164Regex.FindSubmatch(lastNetworkData) + if len(matches) != 2 { + t.Errorf("Failed to extract timestamp from message: %s", string(lastNetworkData)) + } else { + gotTimeStr := string(matches[1]) + if gotTimeStr != expectedTimeStr { + t.Errorf("Timestamp mismatch. Expected: %s, Got: %s", expectedTimeStr, gotTimeStr) + } + } + + config.CleanupConfigAndEnvironment() +} + +func TestTCPRFC5424Output(t *testing.T) { + listener, done := setupTestTCPServer("8092") + defer listener.Close() + + configStr := ` +global: + output: + outputter: network + protocol: tcp + outputTemplate: rfc5424 + endpoints: + - localhost:8092 +samples: + - name: outputnetworksample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + + lines: + - sourcetype: networktest + source: gogen + host: gogen + index: main + priority: "14" + appName: "gogen" + pid: "12345" + tag: "gogen" + custom_field1: "value1" + custom_field2: "value2" + _raw: test message +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + run.Run(c) + + // Wait for the server to receive data + <-done + + // Verify the data was received + if len(lastNetworkData) == 0 { + t.Fatal("No data received over TCP") + } + + // Get expected timestamp in local time with offset + expectedTime := time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local) + _, offset := expectedTime.Zone() + offsetHours := offset / 3600 + offsetMinutes := (offset % 3600) / 60 + var expectedTimeStr string + if offset == 0 { + // For UTC timezone + expectedTimeStr = expectedTime.Format("2006-01-02T15:04:05") + "Z" + } else if offsetHours >= 0 { + expectedTimeStr = fmt.Sprintf("%s+%02d:%02d", expectedTime.Format("2006-01-02T15:04:05"), offsetHours, offsetMinutes) + } else { + expectedTimeStr = fmt.Sprintf("%s-%02d:%02d", expectedTime.Format("2006-01-02T15:04:05"), -offsetHours, offsetMinutes) + } + + // RFC5424 format: 1 timestamp hostname appname pid - [meta key="value"...] message + // Extract timestamp with regex - now supporting both offset format and Z format + timestampRegex := regexp.MustCompile(`^<14>1\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[-+]\d{2}:\d{2}|Z))\s+`) + matches := timestampRegex.FindSubmatch(lastNetworkData) + if len(matches) != 2 { + t.Errorf("Failed to extract timestamp from message: %s", string(lastNetworkData)) + } else { + gotTimeStr := string(matches[1]) + if gotTimeStr != expectedTimeStr { + t.Errorf("Timestamp mismatch. Expected: %s, Got: %s", expectedTimeStr, gotTimeStr) + } + } + + // Full message format validation + rfc5424Regex := regexp.MustCompile(`^<14>1\s+\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[-+]\d{2}:\d{2}|Z)\s+gogen\s+gogen\s+12345\s+-\s+\[meta\s+(?:[a-zA-Z0-9_]+="[^"]*"\s*)+\]\s+test message$`) + + if !rfc5424Regex.Match(lastNetworkData) { + t.Errorf("RFC5424 format mismatch. Got: %s", string(lastNetworkData)) + } + + // Validate meta fields + metaStr := string(lastNetworkData) + expectedMetaFields := []string{ + `custom_field1="value1"`, + `custom_field2="value2"`, + `sourcetype="networktest"`, + `source="gogen"`, + `index="main"`, + } + + for _, field := range expectedMetaFields { + if !strings.Contains(metaStr, field) { + t.Errorf("Missing expected meta field: %s", field) + } + } + + config.CleanupConfigAndEnvironment() +} diff --git a/tests/outputcache/outputcache.yml b/tests/outputcache/outputcache.yml deleted file mode 100644 index 52c0fac..0000000 --- a/tests/outputcache/outputcache.yml +++ /dev/null @@ -1,18 +0,0 @@ -global: - output: - outputter: buf - cacheIntervals: 2 -samples: - - name: outputcache - begin: "2001-10-20 12:00:00" - end: "2001-10-20 12:00:04" - interval: 1 - count: 1 - tokens: - - name: ts1 - type: timestamp - replacement: "%Y-%m-%dT%H:%M:%S" - token: $ts1$ - format: template - lines: - - "_raw": "$ts1$" \ No newline at end of file diff --git a/tests/outputcache_test.go b/tests/outputcache_test.go index 589e8f8..29a80b1 100644 --- a/tests/outputcache_test.go +++ b/tests/outputcache_test.go @@ -1,8 +1,6 @@ package tests import ( - "os" - "path/filepath" "testing" config "github.com/coccyx/gogen/internal" @@ -11,12 +9,28 @@ import ( ) func TestOutputCache(t *testing.T) { - // Setup environment - config.ResetConfig() - os.Setenv("GOGEN_HOME", "..") - os.Setenv("GOGEN_ALWAYS_REFRESH", "") - home := ".." - os.Setenv("GOGEN_FULLCONFIG", filepath.Join(home, "tests", "outputcache", "outputcache.yml")) + configStr := ` +global: + output: + outputter: buf + cacheIntervals: 2 +samples: + - name: outputcache + begin: "2001-10-20 12:00:00" + end: "2001-10-20 12:00:04" + interval: 1 + count: 1 + tokens: + - name: ts1 + type: timestamp + replacement: "%Y-%m-%dT%H:%M:%S" + token: $ts1$ + format: template + lines: + - "_raw": "$ts1$" +` + + config.SetupFromString(configStr) c := config.NewConfig() run.Run(c) @@ -26,4 +40,6 @@ func TestOutputCache(t *testing.T) { 2001-10-20T12:00:00 2001-10-20T12:00:03 `, c.Buf.String()) + + config.CleanupConfigAndEnvironment() } diff --git a/tests/replay/fullreplay.yml b/tests/replay/fullreplay.yml deleted file mode 100644 index 1fcb898..0000000 --- a/tests/replay/fullreplay.yml +++ /dev/null @@ -1,20 +0,0 @@ -global: - output: - outputter: buf -samples: - - name: fullreplay - generator: replay - begin: "2001-10-20 12:00:00" - end: "2001-10-20 12:00:49" - tokens: - - name: ts1 - type: timestamp - replacement: "%Y-%m-%dT%H:%M:%S" - format: regex - token: "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})" - lines: - - "_raw": "2001-10-20T12:00:00" - - "_raw": "2001-10-20T12:00:01" - - "_raw": "2001-10-20T12:00:06" - - "_raw": "2001-10-20T12:00:16" - - "_raw": "2001-10-20T12:00:36" \ No newline at end of file diff --git a/tests/replay_test.go b/tests/replay_test.go index 84c11f9..1da5393 100644 --- a/tests/replay_test.go +++ b/tests/replay_test.go @@ -1,8 +1,6 @@ package tests import ( - "os" - "path/filepath" "testing" config "github.com/coccyx/gogen/internal" @@ -13,10 +11,28 @@ import ( func TestReplay(t *testing.T) { // Setup environment config.ResetConfig() - os.Setenv("GOGEN_HOME", "..") - os.Setenv("GOGEN_ALWAYS_REFRESH", "") - home := ".." - os.Setenv("GOGEN_FULLCONFIG", filepath.Join(home, "tests", "replay", "fullreplay.yml")) + config.SetupFromString(` +global: + output: + outputter: buf +samples: + - name: fullreplay + generator: replay + begin: "2001-10-20 12:00:00" + end: "2001-10-20 12:00:49" + tokens: + - name: ts1 + type: timestamp + replacement: "%Y-%m-%dT%H:%M:%S" + format: regex + token: "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})" + lines: + - "_raw": "2001-10-20T12:00:00" + - "_raw": "2001-10-20T12:00:01" + - "_raw": "2001-10-20T12:00:06" + - "_raw": "2001-10-20T12:00:16" + - "_raw": "2001-10-20T12:00:36" +`) c := config.NewConfig() run.Run(c) @@ -27,4 +43,5 @@ func TestReplay(t *testing.T) { 2001-10-20T12:00:16 2001-10-20T12:00:36 `, c.Buf.String()) + config.CleanupConfigAndEnvironment() } diff --git a/tests/stdout_test.go b/tests/stdout_test.go new file mode 100644 index 0000000..841737b --- /dev/null +++ b/tests/stdout_test.go @@ -0,0 +1,211 @@ +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "strings" + "testing" + "time" + + config "github.com/coccyx/gogen/internal" + "github.com/coccyx/gogen/run" + "github.com/stretchr/testify/assert" +) + +func TestStdoutJSONOutput(t *testing.T) { + // Redirect stdout to capture output + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Clean up at the end + defer func() { + os.Stdout = oldStdout + }() + + configStr := ` +global: + debug: false + verbose: false + output: + outputter: stdout + outputTemplate: json +samples: + - name: stdoutsample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: stdouttest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ + field1: value1 + field2: value2 +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + // Create a channel to signal when Run is complete + done := make(chan bool) + + // Run in a goroutine so we can coordinate output capture + go func() { + run.Run(c) + w.Close() // Close the write end after Run completes + done <- true + }() + + // Read the captured output + var buf bytes.Buffer + _, err := buf.ReadFrom(r) + assert.NoError(t, err) + + // Wait for Run to complete + <-done + + output := strings.TrimSpace(buf.String()) + + // There should be exactly one line of output + lines := strings.Split(output, "\n") + assert.Equal(t, 1, len(lines), "Expected exactly one line of output") + + // Parse the JSON output + var jsonData map[string]interface{} + err = json.Unmarshal([]byte(lines[0]), &jsonData) + assert.NoError(t, err, "Failed to parse JSON output") + + // Validate the expected fields + expectedFields := []string{"sourcetype", "source", "host", "index", "_time", "_raw", "field1", "field2"} + for _, field := range expectedFields { + assert.Contains(t, jsonData, field, "Missing expected field %s in JSON output", field) + } + + // Validate specific field values + assert.Equal(t, "stdouttest", jsonData["sourcetype"]) + assert.Equal(t, "gogen", jsonData["source"]) + assert.Equal(t, "gogen", jsonData["host"]) + assert.Equal(t, "main", jsonData["index"]) + assert.Equal(t, "value1", jsonData["field1"]) + assert.Equal(t, "value2", jsonData["field2"]) + + // Check _time is correct epoch for 2001-10-20 00:00:00 + expectedEpoch := fmt.Sprintf("%.3f", float64(time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local).Unix())) + assert.Equal(t, expectedEpoch, jsonData["_time"]) + + // Check _raw has correct timestamp format + expectedRaw := "20/Oct/2001 00:00:00:000" + assert.Equal(t, expectedRaw, jsonData["_raw"]) + + config.CleanupConfigAndEnvironment() +} + +func TestStdoutCustomTemplateOutput(t *testing.T) { + // Redirect stdout to capture output + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Clean up at the end + defer func() { + os.Stdout = oldStdout + }() + + configStr := ` +global: + debug: false + verbose: false + output: + outputter: stdout + outputTemplate: customtemplate +samples: + - name: stdouttemplatesample + begin: 2001-10-20 00:00:00 + end: 2001-10-20 00:00:01 + interval: 1 + count: 1 + tokens: + - name: ts-dmyhmsms-template + format: template + token: $ts$ + type: timestamp + replacement: "%d/%b/%Y %H:%M:%S:%L" + - name: tsepoch + format: template + token: $epochts$ + field: _time + type: timestamp + replacement: "%s.%L" + + lines: + - sourcetype: stdouttest + source: gogen + host: gogen + index: main + _time: $epochts$ + _raw: $ts$ + field1: custom1 + field2: custom2 + +templates: + - name: customtemplate + header: "" + row: "{{.host}} [{{._time}}] {{.sourcetype}}: {{._raw}} (fields: {{.field1}}, {{.field2}})" + footer: "" +` + + config.SetupFromString(configStr) + c := config.NewConfig() + + // Create a channel to signal when Run is complete + done := make(chan bool) + + // Run in a goroutine so we can coordinate output capture + go func() { + run.Run(c) + w.Close() // Close the write end after Run completes + done <- true + }() + + // Read the captured output + var buf bytes.Buffer + _, err := buf.ReadFrom(r) + assert.NoError(t, err) + + // Wait for Run to complete + <-done + + output := strings.TrimSpace(buf.String()) + + // There should be exactly one line of output + lines := strings.Split(output, "\n") + assert.Equal(t, 1, len(lines), "Expected exactly one line of output") + + // Build expected output string + expectedTime := fmt.Sprintf("%.3f", float64(time.Date(2001, 10, 20, 0, 0, 0, 0, time.Local).Unix())) + expectedOutput := fmt.Sprintf("gogen [%s] stdouttest: 20/Oct/2001 00:00:00:000 (fields: custom1, custom2)", expectedTime) + + // Validate the output matches our expected format + assert.Equal(t, expectedOutput, output, "Output does not match expected format") + + config.CleanupConfigAndEnvironment() +} diff --git a/tests/timer/realtimereplay.yml b/tests/timer/realtimereplay.yml new file mode 100644 index 0000000..0203a7e --- /dev/null +++ b/tests/timer/realtimereplay.yml @@ -0,0 +1,13 @@ +name: realtimereplay +begin: now +generator: replay +tokens: + - name: ts1 + type: timestamp + replacement: "%Y-%m-%dT%H:%M:%S" + format: regex + token: "(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2})" +lines: + - "_raw": "2001-10-20T12:00:00" + - "_raw": "2001-10-20T12:00:01" + - "_raw": "2001-10-20T12:00:03" \ No newline at end of file diff --git a/timer/timer_test.go b/timer/timer_test.go index 40ba0c4..b93ac0c 100644 --- a/timer/timer_test.go +++ b/timer/timer_test.go @@ -43,6 +43,37 @@ func TestTimer(t *testing.T) { assert.Equal(t, true, gt) } +func TestRealtimeReplay(t *testing.T) { + os.Setenv("GOGEN_HOME", "..") + os.Setenv("GOGEN_ALWAYS_REFRESH", "1") + home := filepath.Join("..", "tests", "timer") + os.Setenv("GOGEN_SAMPLES_DIR", home) + + s := tests.FindSampleInFile(home, "realtimereplay") + + gq := make(chan *config.GenQueueItem, 1000) + oq := make(chan *config.OutQueueItem) + done := make(chan int) + gqs := make([]*config.GenQueueItem, 0, 10) + + timer := &Timer{S: s, GQ: gq, OQ: oq, Done: done} + go timer.NewTimer(0) + + time.Sleep(4 * time.Second) + +Loop: + for { + select { + case i := <-gq: + gqs = append(gqs, i) + default: + break Loop + } + } + // Should loop back around to the first entry and play it again + assert.Equal(t, 4, len(gqs)) +} + func TestBackfill(t *testing.T) { os.Setenv("GOGEN_HOME", "..") os.Setenv("GOGEN_ALWAYS_REFRESH", "1") @@ -167,3 +198,44 @@ Loop: } } } + +func TestTimerClose(t *testing.T) { + os.Setenv("GOGEN_HOME", "..") + os.Setenv("GOGEN_ALWAYS_REFRESH", "1") + home := filepath.Join("..", "tests", "timer") + os.Setenv("GOGEN_SAMPLES_DIR", home) + + s := tests.FindSampleInFile(home, "backfillrealtime") + + gq := make(chan *config.GenQueueItem, 1000) + oq := make(chan *config.OutQueueItem) + done := make(chan int) + gqs := make([]*config.GenQueueItem, 0, 10) + + timer := &Timer{S: s, GQ: gq, OQ: oq, Done: done} + go timer.NewTimer(2) + + // Let a few events generate + time.Sleep(100 * time.Millisecond) + + // Close the timer + timer.Close() + close(done) + + // Give it a moment to shut down + time.Sleep(50 * time.Millisecond) + +Loop: + for { + select { + case i := <-gq: + gqs = append(gqs, i) + default: + break Loop + } + } + + // Verify we got some events but the timer stopped + assert.Greater(t, len(gqs), 0) + assert.Less(t, len(gqs), 100) // Sanity check that timer actually stopped +} From 0e2e5e47a657a97d26a84db36e685ed40e2e56fa Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Mon, 24 Feb 2025 18:39:53 -0800 Subject: [PATCH 02/51] Adding deploy steps --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdf71c2..be1746e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,18 +38,18 @@ jobs: run: docker build -t clintsharp/gogen . # Deployment: These steps run only on the main branch. - # - name: Configure AWS Credentials - # if: github.ref == 'refs/heads/main' - # uses: aws-actions/configure-aws-credentials@v1 - # with: - # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - # aws-region: us-west-1 - - # - name: Deploy Build Artifacts to S3 - # if: github.ref == 'refs/heads/main' - # run: aws s3 sync build s3://gogen-artifacts --delete - - # - name: Run Docker Push Script - # if: github.ref == 'refs/heads/main' - # run: bash docker-push.sh \ No newline at end of file + - name: Configure AWS Credentials + if: github.ref == 'refs/heads/master' + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-1 + + - name: Deploy Build Artifacts to S3 + if: github.ref == 'refs/heads/master' + run: aws s3 sync build s3://gogen-artifacts --delete + + - name: Run Docker Push Script + if: github.ref == 'refs/heads/master' + run: bash docker-push.sh \ No newline at end of file From a01295c64f7b1c23134aa6fb2e9da2fef8c29831 Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Thu, 27 Feb 2025 17:01:53 -0800 Subject: [PATCH 03/51] Removed modinput and Splunk app support (#47) --- README/Examples.md | 1 - README/Reference.md | 2 +- README/TODO.md | 1 - internal/config.go | 3 +- internal/defaults.go | 7 - splunk_app_gogen/README.md | 42 --- splunk_app_gogen/README/inputs.conf.spec | 25 -- splunk_app_gogen/RELEASE_NOTES.md | 3 - splunk_app_gogen/bin/gogen.py | 331 ------------------ splunk_app_gogen/default/app.conf | 12 - .../default/data/ui/manager/gogen_manager.xml | 89 ----- .../gogen_assets/configs/geopath.yml | 38 -- .../configs/retail_transaction.yml | 134 ------- .../gogen_assets/configs/trig_metrics.yml | 31 -- .../gogen_assets/generators/geopath.lua | 131 ------- .../generators/retail_transaction.lua | 265 -------------- .../gogen_assets/samples/external_ips.sample | 150 -------- .../gogen_assets/samples/itemcode.sample | 8 - .../gogen_assets/samples/paymenttype.sample | 3 - .../gogen_assets/samples/useragents.sample | 84 ----- .../gogen_assets/samples/webhosts.csv | 4 - splunk_app_gogen/local/inputs.conf | 15 - splunk_app_gogen/metadata/default.meta | 2 - splunk_app_gogen/static/appIcon.png | Bin 811 -> 0 bytes splunk_app_gogen/static/appIconAlt.png | Bin 811 -> 0 bytes splunk_app_gogen/static/appIconAlt_2x.png | Bin 1145 -> 0 bytes splunk_app_gogen/static/appIcon_2x .png | Bin 1145 -> 0 bytes template/template.go | 24 -- 28 files changed, 2 insertions(+), 1403 deletions(-) delete mode 100644 splunk_app_gogen/README.md delete mode 100644 splunk_app_gogen/README/inputs.conf.spec delete mode 100644 splunk_app_gogen/RELEASE_NOTES.md delete mode 100644 splunk_app_gogen/bin/gogen.py delete mode 100644 splunk_app_gogen/default/app.conf delete mode 100644 splunk_app_gogen/default/data/ui/manager/gogen_manager.xml delete mode 100644 splunk_app_gogen/gogen_assets/configs/geopath.yml delete mode 100644 splunk_app_gogen/gogen_assets/configs/retail_transaction.yml delete mode 100644 splunk_app_gogen/gogen_assets/configs/trig_metrics.yml delete mode 100644 splunk_app_gogen/gogen_assets/generators/geopath.lua delete mode 100644 splunk_app_gogen/gogen_assets/generators/retail_transaction.lua delete mode 100644 splunk_app_gogen/gogen_assets/samples/external_ips.sample delete mode 100644 splunk_app_gogen/gogen_assets/samples/itemcode.sample delete mode 100644 splunk_app_gogen/gogen_assets/samples/paymenttype.sample delete mode 100644 splunk_app_gogen/gogen_assets/samples/useragents.sample delete mode 100644 splunk_app_gogen/gogen_assets/samples/webhosts.csv delete mode 100644 splunk_app_gogen/local/inputs.conf delete mode 100644 splunk_app_gogen/metadata/default.meta delete mode 100644 splunk_app_gogen/static/appIcon.png delete mode 100644 splunk_app_gogen/static/appIconAlt.png delete mode 100644 splunk_app_gogen/static/appIconAlt_2x.png delete mode 100644 splunk_app_gogen/static/appIcon_2x .png diff --git a/README/Examples.md b/README/Examples.md index 9d4a942..91a4fb6 100644 --- a/README/Examples.md +++ b/README/Examples.md @@ -8,6 +8,5 @@ In addition to our tutorial, we've put together a number of good example configs | [CSV](../examples/csv/csv.yml) | Generates CSV Data. Showcases Gogen can be used for use cases aside from time series data. | [UNIX](https://github.com/coccyx/gogen/tree/master/examples/nixOS) | This example showcases custom generators. Generates data like running `df`, `ps`, etc on a UNIX box the way Splunk's UNIX TA collects data. Variables correlate and make heavy use of the LUA Generators feature. | [Users](https://github.com/coccyx/gogen/tree/master/examples/generator) | This example also showcases LUA Generators. Simulates a group of users going through a set of actions. Show cases complex interplay that's only possible by writing code. -| [Splunktel Demo](https://github.com/coccyx/splunktel_demo) | This example uses Gogen as a Splunk app and has a five relevant configs for Gogen stored in gogen_assets. Shows to how to make a composable, managable Gogen config. If you have relevant examples, send a PR and we'll add them to this list! diff --git a/README/Reference.md b/README/Reference.md index effe3c7..d5ae3e7 100644 --- a/README/Reference.md +++ b/README/Reference.md @@ -60,7 +60,7 @@ Output options: | backupFiles | For file output, sets the number of files to keep before discarding older files | int | | bufferBytes | For HTTP, S2S and other outputs, sets the number of bytes to buffer before flushing | int | | outputter | Sets the output module to use, currently supports devnull, file, http and stdout | string | -| outputTemplate | Set the output template to format output, builtins include csv, json, splunkhec, modinput | string | +| outputTemplate | Set the output template to format output, builtins include csv, json, splunkhec | string | | endpoints | For http, or potentially others, lists endpoints to send data to. | string list | | headers | For http, sets headers | string obj | | protocol | For network, set to `tcp` or `udp` | string | diff --git a/README/TODO.md b/README/TODO.md index 6fd41aa..5053f39 100644 --- a/README/TODO.md +++ b/README/TODO.md @@ -26,7 +26,6 @@ * Manipulate running state through REST * Configuration UI -* Add md5sum for gogen exe's and download new gogen from modinput if md5sum does not match * Add timemultiple * Consider finding a way to break up config package and refactor using better interface design * Unit test coverage 90% diff --git a/internal/config.go b/internal/config.go index 7e985e8..04c2da7 100644 --- a/internal/config.go +++ b/internal/config.go @@ -257,7 +257,7 @@ func BuildConfig(cc ConfigConfig) *Config { c.Global.Output.channelMap = make(map[string]int) // Add default templates - templates := []*Template{defaultCSVTemplate, defaultJSONTemplate, defaultSplunkHECTemplate, defaultRawTemplate, defaultModinputTemplate} + templates := []*Template{defaultCSVTemplate, defaultJSONTemplate, defaultSplunkHECTemplate, defaultRawTemplate} c.Templates = append(c.Templates, templates...) for _, t := range c.Templates { if len(t.Header) > 0 { @@ -1093,7 +1093,6 @@ func (c *Config) SetupSystemTokens() { } syslogOutput := c.Global.Output.OutputTemplate == "rfc3164" || c.Global.Output.OutputTemplate == "rfc5424" addTime := c.Global.Output.OutputTemplate == "splunkhec" || - c.Global.Output.OutputTemplate == "modinput" || c.Global.Output.OutputTemplate == "elasticsearch" || c.Global.AddTime || syslogOutput diff --git a/internal/defaults.go b/internal/defaults.go index d2bb7bd..aadba83 100644 --- a/internal/defaults.go +++ b/internal/defaults.go @@ -52,7 +52,6 @@ var ( defaultJSONTemplate *Template defaultSplunkHECTemplate *Template defaultRawTemplate *Template - defaultModinputTemplate *Template defaultRaterConfig *RaterConfig defaultConfigRaterConfig *RaterConfig @@ -83,12 +82,6 @@ func init() { Row: `{{ ._raw }}`, Footer: "", } - defaultModinputTemplate = &Template{ - Name: "modinput", - Header: "", - Row: `{{ modinput . | printf "%s" }}`, - Footer: "", - } defaultRaterConfig = &RaterConfig{ Name: "default", diff --git a/splunk_app_gogen/README.md b/splunk_app_gogen/README.md deleted file mode 100644 index d467662..0000000 --- a/splunk_app_gogen/README.md +++ /dev/null @@ -1,42 +0,0 @@ -## GOGEN Modular Input Wrapper v0.5 - -## Overview - -This app is Splunk Modular Input wrapper for streaming GoGen generated events : https://github.com/coccyx/gogen - -## Dependencies - -* Splunk version 5+ -* Python runtime if installing on a Universal Forwarder -* Supported on OSX , Windows , Linux -* Network access to http://api.gogen.io to download the gogen executable, this will by dynamically downloaded for you the first time you run the Modular Input and placed in the splunk_app_gogen/bin directory.If you want to update this executable , simply delete the previously downloaded executable and restart the Modular Input stanza. - -## Setup - -* Untar the release to your `$SPLUNK_HOME/etc/apps` directory -* Restart Splunk -* Browse to Settings -> Data Inputs -> Gogen to setup a new stanza - -## Modular Input Configuration - -* Descriptions of the fields you can configure in your stanzas in inputs.conf are in README/inputs.conf.spec and also annotated in the web interface if you are setting up your stanzas there.These fields are basically mappings through to the various command line arguments you can pass to the gogen executable. - -## Gogen Configuration - -* Documentation can be found here : https://github.com/coccyx/gogen/blob/master/README.md -* YAML configuration files, samples files and custom lua generator scripts can be placed in splunk_app_gogen/gogen_assets - -## Logging - -Standard logging is written to SPLUNK_HOME/var/log/splunk/gogen.log - -Any system errors can be searched for : index=_internal error gogen.py - -## Troubleshooting - -* You are using Splunk version 5+ ? -* Look for any errors in the logs -* Any firewalls blocking outgoing HTTP calls to retrieve the gogen binary ? -* Are you running on a supported OS platform ? -* If running on a Universal Forwarder , do you have a Python 2.7 runtime installed ? -* Was the gogen executable downloaded to splunk_app_gogen/bin ? \ No newline at end of file diff --git a/splunk_app_gogen/README/inputs.conf.spec b/splunk_app_gogen/README/inputs.conf.spec deleted file mode 100644 index 07caab2..0000000 --- a/splunk_app_gogen/README/inputs.conf.spec +++ /dev/null @@ -1,25 +0,0 @@ -[gogen://] - -config = - * Short Gogen path (coccyx/weblog for example), full file path,local file in config directory, or URL pointing to YAML or JSON config. - -config_type = - * local_file / short_path / full_file_path / url - -count = - * Count of events to generate every interval. Overrides any amounts set in the Gogen config. - -gogen_interval = - * Generate events every interval seconds. Overrides any interval set in the Gogen config. - -end_intervals = - * Generate events for endIntervals and stop. Overrides any endInterval set in the Gogen config. - -begin = - * Start generating events at begin time. Can use Splunk's relative time syntax or an absolute time. Overrides any begin setting in the Gogen config. - -end = - * End generating events at end time. Can use Splunk's relative time syntax or an absolute time. Overrides any end setting in the Gogen config. - -generator_threads = - * Sets number of generator threads diff --git a/splunk_app_gogen/RELEASE_NOTES.md b/splunk_app_gogen/RELEASE_NOTES.md deleted file mode 100644 index 8ae0ad1..0000000 --- a/splunk_app_gogen/RELEASE_NOTES.md +++ /dev/null @@ -1,3 +0,0 @@ -0.5 ------ -Initial beta release diff --git a/splunk_app_gogen/bin/gogen.py b/splunk_app_gogen/bin/gogen.py deleted file mode 100644 index 2296d63..0000000 --- a/splunk_app_gogen/bin/gogen.py +++ /dev/null @@ -1,331 +0,0 @@ -from __future__ import division -import sys -import os -import xml.dom.minidom -import subprocess -import logging -import logging.handlers -import platform -import urllib - - -def setupLogger(logger=None, log_format='%(asctime)s %(levelname)s [Gogen] %(message)s', level=logging.INFO, log_name="gogen.log", logger_name="gogen"): - """ - Setup a logger suitable for splunkd consumption - """ - if logger is None: - logger = logging.getLogger(logger_name) - - # Prevent the log messages from being duplicated in the python.log file - logger.propagate = False - logger.setLevel(level) - - file_handler = logging.handlers.RotatingFileHandler(os.path.join( - os.environ['SPLUNK_HOME'], 'var', 'log', 'splunk', log_name), maxBytes=2500000, backupCount=5) - formatter = logging.Formatter(log_format) - file_handler.setFormatter(formatter) - - logger.handlers = [] - logger.addHandler(file_handler) - - logger.debug("init %s logger", logger_name) - return logger - -SCHEME = """ - Gogen - Generate data using Gogen - true - false - xml - - - - GoGen input name - Name of this GoGen input - - - - Configuration Descriptor Type - The type of config defined in the Configuration Descriptor field. Defaults to config_dir. - false - false - - - Configuration Descriptor - Short Gogen path (coccyx/weblog for example), full file path,local file in config directory, or URL pointing to YAML or JSON config. Leave blank to use all configs in gogen_assets. - false - false - - - - Count - Count of events to generate every interval. Overrides any amounts set in the Gogen config - false - false - - - Interval - Generate events every interval seconds. Overrides any interval set in the Gogen config - false - false - - - End Intervals - Generate events for endIntervals and stop. Overrides any endInterval set in the Gogen config - false - false - - - Begin - Start generating events at begin time. Can use Splunk's relative time syntax or an absolute time. Overrides any begin setting in the Gogen config - false - false - - - End - End generating events at end time. Can use Splunk's relative time syntax or an absolute time. Overrides any end setting in the Gogen config - false - false - - - Generator Threads - Sets number of generator threads - false - false - - - - - -""" - - -def do_validate(): - config = get_validation_config() - # TODO - # if error , print_validation_error & sys.exit(2) - -# prints validation error data to be consumed by Splunk - - -def print_validation_error(s): - print "%s" % encodeXMLText(s) - - -def encodeXMLText(text): - text = text.replace("&", "&") - text = text.replace("\"", """) - text = text.replace("'", "'") - text = text.replace("<", "<") - text = text.replace(">", ">") - return text - - -def usage(): - print "usage: %s [--scheme|--validate-arguments]" - logger.error("Incorrect Program Usage") - sys.exit(2) - - -def do_scheme(): - print SCHEME - - -# read XML configuration passed from splunkd -def get_config(): - config = {} - - try: - # read everything from stdin - config_str = sys.stdin.read() - logger.debug("Config Str: %s" % config_str) - - # parse the config XML - doc = xml.dom.minidom.parseString(config_str) - root = doc.documentElement - server_host = str(root.getElementsByTagName( - "server_host")[0].firstChild.data) - if server_host: - logger.debug("XML: Found server_host") - config["server_host"] = server_host - server_uri = str(root.getElementsByTagName( - "server_uri")[0].firstChild.data) - if server_uri: - logger.debug("XML: Found server_uri") - config["server_uri"] = server_uri - session_key = str(root.getElementsByTagName( - "session_key")[0].firstChild.data) - if session_key: - logger.debug("XML: Found session_key") - config["session_key"] = session_key - checkpoint_dir = str(root.getElementsByTagName( - "checkpoint_dir")[0].firstChild.data) - if checkpoint_dir: - logger.debug("XML: Found checkpoint_dir") - config["checkpoint_dir"] = checkpoint_dir - conf_node = root.getElementsByTagName("configuration")[0] - if conf_node: - logger.debug("XML: found configuration") - stanza = conf_node.getElementsByTagName("stanza")[0] - if stanza: - stanza_name = stanza.getAttribute("name") - if stanza_name: - logger.debug("XML: found stanza " + stanza_name) - config["name"] = stanza_name - - params = stanza.getElementsByTagName("param") - for param in params: - param_name = param.getAttribute("name") - logger.debug("XML: found param '%s'" % param_name) - if param_name and param.firstChild and \ - param.firstChild.nodeType == param.firstChild.TEXT_NODE: - data = param.firstChild.data - config[param_name] = data - logger.debug("XML: '%s' -> '%s'" % - (param_name, data)) - - checkpnt_node = root.getElementsByTagName("checkpoint_dir")[0] - if checkpnt_node and checkpnt_node.firstChild and \ - checkpnt_node.firstChild.nodeType == checkpnt_node.firstChild.TEXT_NODE: - config["checkpoint_dir"] = checkpnt_node.firstChild.data - - if not config: - raise Exception, "Invalid configuration received from Splunk." - - # just some validation: make sure these keys are present (required) - # validate_conf(config, "name") - # validate_conf(config, "key_id") - # validate_conf(config, "secret_key") - # validate_conf(config, "checkpoint_dir") - except Exception, e: - raise Exception, "Error getting Splunk configuration via STDIN: %s" % str( - e) - - return config - -# read XML configuration passed from splunkd, need to refactor to support -# single instance mode - - -def get_validation_config(): - val_data = {} - - # read everything from stdin - val_str = sys.stdin.read() - - # parse the validation XML - doc = xml.dom.minidom.parseString(val_str) - root = doc.documentElement - - logger.debug("XML: found items") - item_node = root.getElementsByTagName("item")[0] - if item_node: - logger.debug("XML: found item") - - name = item_node.getAttribute("name") - val_data["stanza"] = name - - params_node = item_node.getElementsByTagName("param") - for param in params_node: - name = param.getAttribute("name") - logger.debug("Found param %s" % name) - if name and param.firstChild and \ - param.firstChild.nodeType == param.firstChild.TEXT_NODE: - val_data[name] = param.firstChild.data - - return val_data - - -if __name__ == '__main__': - logger = setupLogger(level=logging.DEBUG) - - if len(sys.argv) > 1: - if sys.argv[1] == "--scheme": - do_scheme() - elif sys.argv[1] == "--validate-arguments": - do_validate() - else: - usage() - sys.exit(0) - else: - config = get_config() - - if platform.system() == 'Linux': - exefile = 'gogen_real' - gogen_url = 'https://api.gogen.io/linux/gogen' - elif platform.system() == 'Windows': - exefile = 'gogen_real.exe' - gogen_url = 'https://api.gogen.io/windows/gogen.exe' - else: - exefile = 'gogen_real' - gogen_url = 'https://api.gogen.io/osx/gogen' - - # gogen_path = os.path.join( - # os.environ['SPLUNK_HOME'], 'etc', 'apps', 'splunk_app_gogen', 'bin', - # exefile) - gogen_base_path = os.path.sep.join( - os.path.realpath(__file__).split(os.path.sep)[0:-2]) - gogen_path = os.path.join(gogen_base_path, 'bin', exefile) - if not os.path.exists(gogen_path): - urllib.urlretrieve(gogen_url, gogen_path) - os.chmod(gogen_path, 0755) - - args = [] - args.append(gogen_path) - # args.append('-v') - args.append('-ot') - args.append('modinput') - - if 'config_type' in config: - config_type = str(config['config_type']) - else: - config_type = 'config_dir' - - if config_type == 'config_dir': - args.append('-cd') - args.append(os.path.join(gogen_base_path, 'gogen_assets')) - else: - args.append('-sd') - args.append(os.path.join(gogen_base_path, - 'gogen_assets', 'samples') + os.path.sep) - if 'config' in config: - args.append('-c') - config_file = str(config['config']) - if config_type == 'local_file': - args.append(os.path.join( - gogen_base_path, 'gogen_assets','configs', config_file)) - else: - args.append(config_file) - - if 'generator_threads' in config: - args.append('-g') - args.append(str(config['generator_threads'])) - - args.append('gen') - - if 'count' in config: - args.append('-c') - args.append(str(config['count'])) - if 'gogen_interval' in config: - args.append('-i') - args.append(str(config['gogen_interval'])) - if 'end_intervals' in config: - args.append('-ei') - args.append(str(config['end_intervals'])) - if 'begin' in config: - args.append('-b') - args.append(str(config['begin'])) - if 'end' in config: - args.append('-e') - args.append(str(config['end'])) - # if 'begin' not in config and 'end' not in config and 'end_intervals' not in config: - # args.append('-r') - - import pprint - logger.debug('args: %s' % pprint.pformat(args)) - logger.debug('command: %s' % ' '.join(args)) - - sys.stdout.write("\n") - sys.stdout.flush() - p = subprocess.Popen(args, cwd=gogen_base_path, - shell=False) diff --git a/splunk_app_gogen/default/app.conf b/splunk_app_gogen/default/app.conf deleted file mode 100644 index a054051..0000000 --- a/splunk_app_gogen/default/app.conf +++ /dev/null @@ -1,12 +0,0 @@ -[ui] -is_visible = 1 -label = Gogen - -[launcher] -description = Modular Input Splunk wrapper for Gogen -version = 0.5 -author = Clint Sharp - -[package] -check_for_updates = 1 -id = gogen \ No newline at end of file diff --git a/splunk_app_gogen/default/data/ui/manager/gogen_manager.xml b/splunk_app_gogen/default/data/ui/manager/gogen_manager.xml deleted file mode 100644 index 34afe20..0000000 --- a/splunk_app_gogen/default/data/ui/manager/gogen_manager.xml +++ /dev/null @@ -1,89 +0,0 @@ - -
Gogen
- - datainputstats - Gogen - - - - - - - - - - - - - Name of this stanza - - - - - - - Short Gogen path (coccyx/weblog for example), full file path,local file in config directory, or URL pointing to YAML or JSON config. Leave blank to use all configs in gogen_assets. - - - - - - - The type of config defined in the Configuration Descriptor field. Defaults to config_dir. - - - - - - - - - - - - - Count of events to generate every interval. Overrides any amounts set in the Gogen config - - - - - - Generate events every interval seconds. Overrides any interval set in the Gogen config - - - - - - Generate events for endIntervals and stop. Overrides any endInterval set in the Gogen config - - - - - - Start generating events at begin time. Can use Splunk's relative time syntax or an absolute time. Overrides any begin setting in the Gogen config - - - - - - End generating events at end time. Can use Splunk's relative time syntax or an absolute time. Overrides any end setting in the Gogen config - - - - - - Sets number of generator threads - - - - - - - - - - entity['eai:acl']['app'] or "" - - - -
\ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/configs/geopath.yml b/splunk_app_gogen/gogen_assets/configs/geopath.yml deleted file mode 100644 index 7dea9c5..0000000 --- a/splunk_app_gogen/gogen_assets/configs/geopath.yml +++ /dev/null @@ -1,38 +0,0 @@ -generators: - - name: geopath - fileName: $SPLUNK_HOME/etc/apps/splunk_app_gogen/gogen_assets/generators/geopath.lua - singleThreaded: false - init: - # 250 Brannan St - start_lat_decimal: 37.7830564 - start_long_decimal: -122.3932187 - initial_bearing_degrees: 122 - # as a guide , walk speed 1.5 ,driving speed 30, airplane speed 245 - initial_speed_metres_per_sec: 30 - # random (0,360) | straight - bearing_mode: random - # only applied when in random mode - box_containment_top_lat: 37.784 - box_containment_bottom_lat: 37.782 - box_containment_left_long: -122.392 - box_containment_right_long: -122.394 -samples: - - name: geopath - description: Generate a stateful geopath - notes: > - Simulates a "real" geopath - generator: geopath - interval: 5 - count : 1 - tokens: - - name: ts-dmyhmsms-regex - format: regex - token: "(\\d{1,2}/\\w{3}/\\d{4}\\s\\d{2}:\\d{2}:\\d{2}:\\d{1,3})" - type: timestamp - replacement: "%d/%b/%Y %H:%M:%S:%L" - lines: - - index: main - sourcetype: geopath - source: gogen - host: none - _raw: 29/Apr/2013 18:09:05:132 userid=$userid$ lat=$lat$ long=$long$ distancetravelled=$distancetravelled$ speed=$speed$ bearing=$bearing$ \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/configs/retail_transaction.yml b/splunk_app_gogen/gogen_assets/configs/retail_transaction.yml deleted file mode 100644 index 9ed7d49..0000000 --- a/splunk_app_gogen/gogen_assets/configs/retail_transaction.yml +++ /dev/null @@ -1,134 +0,0 @@ -global: - samplesDir: - - $SPLUNK_HOME/etc/apps/splunk_app_gogen/gogen_assets/samples -generators: - - name: retailtransaction - fileName: $SPLUNK_HOME/etc/apps/splunk_app_gogen/gogen_assets/generators/retail_transaction.lua - singleThreaded: false - options: - users: - - john - - tom - - sue - - amy - - fred - - annie - - george - - emily - - rick - - alan - traversaldelay: - 0: 10 - 1: 30 - 2: 30 - 3: 30 - 4: 30 - 5: 30 - 6: 30 - 7: 60 - init: - stage: 0 - cartitemcount: 0 - traversalsteps: 0 - maxtraversalsteps: 100 - cartrepeats: 0 - maxcartrepeats: 5 - sessionid: 0 -samples: - - name: transaction - description: Generate a retail transaction - notes: > - Simulates a retail transaction with statefullness.Simulating multiple users is accomplished by running multiple - generator threads which each maintain their own state.You can set this thread size value in the Modular Input setup. - generator: retailtransaction - interval: 1 - endIntervals: 10 - tokens: - - name: ts-dmyhmsms-regex - format: regex - token: "(\\d{1,2}/\\w{3}/\\d{4}\\s\\d{2}:\\d{2}:\\d{2}:\\d{1,3})" - type: timestamp - replacement: "%d/%b/%Y %H:%M:%S:%L" - - name: host - format: template - type: fieldChoice - srcField: host - field: host - sample: webhosts.csv - - name: clientip - format: template - type: choice - sample: external_ips.sample - - name: useragent - format: template - type: choice - sample: useragents.sample - - name: saleamount - format: template - type: random - replacement: float - precision: 2 - lower: 1.0 - upper: 4000.0 - - name: itemcode - type: choice - sample: itemcode.sample - disabled: true - - name: paymenttype - format: template - type: choice - sample: paymenttype.sample - - name: paymentstatus - format: template - type: weightedChoice - weightedChoice: - - weight: 20 - choice: SUCCESS - - weight: 3 - choice: FAILURE - lines: - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=login sessionid=$session$ clientip=$clientip$ useragent=$useragent$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=landingpage sessionid=$session$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=searchitem sessionid=$session$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=viewitem sessionid=$session$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=additem sessionid=$session$ itemcode=$itemcode$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=checkout sessionid=$session$ itemcount=$itemcount$ saleamount=$saleamount$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=payment paymentstatus=$paymentstatus$ sessionid=$session$ paymenttype=$paymenttype$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=logout sessionid=$session$ - - index: main - sourcetype: retail_transaction - source: gogen - host: $host$ - _raw: 29/Apr/2013 18:09:05:132 user=$user$ action=removeitem sessionid=$session$ itemcode=$itemcode$ diff --git a/splunk_app_gogen/gogen_assets/configs/trig_metrics.yml b/splunk_app_gogen/gogen_assets/configs/trig_metrics.yml deleted file mode 100644 index 756e7fe..0000000 --- a/splunk_app_gogen/gogen_assets/configs/trig_metrics.yml +++ /dev/null @@ -1,31 +0,0 @@ -samples: - - name: trig_metrics - interval: 1 - count: 1 - tokens: - - name: sin - format: template - type: script - script: > - math.randomseed(os.time()) - local x = math.random(1,100) - return math.abs(math.sin(x)) - - name: cos - format: template - type: script - script: > - math.randomseed(os.time()) - local x = math.random(1,100) - return math.abs(math.cos(x)) - - name: tan - format: template - type: script - script: > - math.randomseed(os.time()) - local x = math.random(1,100) - return math.abs(math.tan(x)) - lines: - - _raw: sin=$sin$ cos=$cos$ tan=$tan$ - index: main - sourcetype: trig_metrics - source: gogen \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/generators/geopath.lua b/splunk_app_gogen/gogen_assets/generators/geopath.lua deleted file mode 100644 index 9d9fe5e..0000000 --- a/splunk_app_gogen/gogen_assets/generators/geopath.lua +++ /dev/null @@ -1,131 +0,0 @@ -function getline(t, i) - local count = 0 - for k,v in pairsByKeys(t) do - if count >= i then - return v - else - count = count + 1 - end - end -end - -function pairsByKeys (t, f) - local a = {} - for n in pairs(t) do table.insert(a, n) end - if f ~= nil then - table.sort(a, f) - else - table.sort(a) - end - local i = 0 -- iterator variable - local iter = function () -- iterator function - i = i + 1 - if a[i] == nil then return nil - else return a[i], t[a[i]] - end - end - return iter -end - -function sendevent(i, choices) - l = getline(lines, tonumber(i)) - if choices == nil then - l, ret = replaceTokens(l) - else - l, ret = replaceTokens(l, choices) - end - events = { } - table.insert(events, l) - send(events) - return ret -end - -function sessionid() - -- Generate a unique session id - math.randomseed(os.time()) - local random = math.random - local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - return string.gsub(template, '[xy]', function (c) - local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) - return string.format('%x', v) - end) -end - -function radianstodecimaldegrees(radians) - - return radians * (180 / math.pi) - -end - -function decimaldegreestoradians(degrees) - - return (math.pi / 180) * degrees - -end - -function nextlatlong(lat_decimal,long_decimal,bearing_decimal,distance_metres) - local lat_radians = decimaldegreestoradians(lat_decimal) - local long_radians = decimaldegreestoradians(long_decimal) - local bearing_radians = decimaldegreestoradians(bearing_decimal) - local earths_radius_metres = 6378137 - local lat_radians_next = math.asin(math.sin(lat_radians)*math.cos(distance_metres/earths_radius_metres) + math.cos(lat_radians)*math.sin(distance_metres/earths_radius_metres)*math.cos(bearing_radians)) - local long_radians_next = long_radians + math.atan(math.sin(bearing_radians)*math.sin(distance_metres/earths_radius_metres)*math.cos(lat_radians),math.cos(distance_metres/earths_radius_metres)-math.sin(lat_radians)*math.sin(lat_radians_next)) - - return radianstodecimaldegrees(lat_radians_next),radianstodecimaldegrees(long_radians_next),distance_metres - -end - ---init -if state["userid"] == nil or state["userid"] == 0 then - state["userid"] = sessionid() - setToken("userid", state["userid"]) - state["lat"] = state["start_lat_decimal"] - state["long"] = state["start_long_decimal"] - state["bearing"] = state["initial_bearing_degrees"] - state["speed"] = state["initial_speed_metres_per_sec"] -end -if state["distance"] == nil then - state["distance"] = 0 -end -setToken("lat", state["lat"]) -setToken("long", state["long"]) -setToken("distancetravelled", state["distance"]) -setToken("bearing", state["bearing"]) -setToken("speed", state["speed"]) - ---output event -sendevent(0) - --- get next co-ordinates -state["lat"],state["long"],state["distance"] = nextlatlong(state["lat"],state["long"],state["bearing"],state["speed"]*5) - --- randomly change the bearing for some variability in the path -if state["bearing_mode"] == "random" then - - -- check if we need to apply containment logic - if(state["box_containment_top_lat"] ~= nil and state["box_containment_bottom_lat"] ~= nil and state["box_containment_left_long"] ~= nil and state["box_containment_right_long"] ~= nil) then - -- are we still in the containment box - - if(state["lat"] < state["box_containment_top_lat"] and state["lat"] > state["box_containment_bottom_lat"] and state["long"] < state["box_containment_left_long"] and state["long"] > state["box_containment_right_long"]) then - state["bearing"] = math.random(0, 360) - debug("contained") - else - -- dog left the yard , relect the path to the back bearing - if(state["bearing"] >= 180) then - state["bearing"] = state["bearing"] - 180 - else - state["bearing"] = state["bearing"] + 180 - end - -- get next co-ordinates - state["lat"],state["long"],state["distance"] = nextlatlong(state["lat"],state["long"],state["bearing"],state["speed"]*5) - debug("dog left the yard") - state["bearing"] = math.random(0, 360) - end - else - debug("no containment logic") - state["bearing"] = math.random(0, 360) - end -end -if state["bearing_mode"] == "straight" then - -- no need to do anything -end diff --git a/splunk_app_gogen/gogen_assets/generators/retail_transaction.lua b/splunk_app_gogen/gogen_assets/generators/retail_transaction.lua deleted file mode 100644 index 092d324..0000000 --- a/splunk_app_gogen/gogen_assets/generators/retail_transaction.lua +++ /dev/null @@ -1,265 +0,0 @@ -function countlines(t) - local count = 0 - for k, v in pairs(t) do - count = count + 1 - end - return count -end - -function countentries(ud) - local count = 0 - for k, v in ud() do - count = count + 1 - end - return count -end - -function getline(t, i) - local count = 0 - for k,v in pairsByKeys(t) do - if count >= i then - return v - else - count = count + 1 - end - end -end - -function getentry(ud, i) - local count = 0 - for k, v in ud() do - if count >= i then - return v - else - count = count + 1 - end - end -end - -function pairsByKeys (t, f) - local a = {} - for n in pairs(t) do table.insert(a, n) end - if f ~= nil then - table.sort(a, f) - else - table.sort(a) - end - local i = 0 -- iterator variable - local iter = function () -- iterator function - i = i + 1 - if a[i] == nil then return nil - else return a[i], t[a[i]] - end - end - return iter -end - -function sendevent(i, choices) - l = getline(lines, tonumber(i)) - if choices == nil then - l, ret = replaceTokens(l) - else - l, ret = replaceTokens(l, choices) - end - events = { } - table.insert(events, l) - send(events) - return ret -end - -function getudvalue(ud, key) - for k, v in ud() do - if tostring(k) == key then - return v - end - end -end - -function setcountdown() - -- Countdown a random amount of seconds - local upper = getudvalue(options["traversaldelay"], tostring(state["stage"])) - math.randomseed(os.time()) - local countdown = math.random(1, upper) - state["countdown"] = countdown -end - -function setmaxtraversalsteps() - local upper = state["maxtraversalsteps"] - math.randomseed(os.time()) - local max = math.random(1, upper) - state["maxtraversalsteps"] = max -end - -function setmaxcartrepeats() - local upper = state["maxcartrepeats"] - math.randomseed(os.time()) - local max = math.random(1, upper) - state["maxcartrepeats"] = max -end - -function reset() - state["stage"] = 0 - state["sessionid"] = 0 - state["user"] = nil - state["cartitemcount"] = 0 - state["traversalsteps"] = 0 - state["cartrepeats"] = 0 - state["cart"] = {} -end - -function checkforabort() - if state["traversalsteps"] >= state["maxtraversalsteps"] then - reset() - end -end - -function sessionid() - -- Generate a unique session id - math.randomseed(os.time()) - local random = math.random - local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - return string.gsub(template, '[xy]', function (c) - local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) - return string.format('%x', v) - end) -end - -function getitemcodechoice() - - local itemcodes = getChoice("itemcode") - math.randomseed(os.time()) - local itemline = math.random( 1, (countlines(itemcodes) - 1)) - local item = itemcodes[itemline] - debug("itemline "..itemline) - debug("item "..item) - return item - -end - -function randomremoval() - - math.randomseed(os.time()) - local remainder = math.fmod(math.random(1,100),10) - return remainder == 0 - -end - - -if state["countdown"] == nil or state["countdown"] == 0 then - if randomremoval() then - if state["cartitemcount"] > 0 then - debug("removal") - debug("cart size "..countlines(state["cart"])) - debug("cart size "..state["cartitemcount"]) - math.randomseed(os.time()) - local itemline = math.random( 0, (countlines(state["cart"]) - 1)) - debug("itemline "..itemline) - local itemcode = table.remove(state["cart"],itemline) - debug("itemcode "..itemcode) - state["cartitemcount"] = state["cartitemcount"] - 1 - setToken("itemcode", itemcode) - sendevent(8) - setcountdown() - return - end - end - --login - if state["stage"] == 0 then - debug("stage 0") - setmaxtraversalsteps() - setmaxcartrepeats() - -- Pick a user - if state["user"] == nil then - math.randomseed(os.time()) - local userline = math.random( 0, countentries(options["users"])-1 ) - debug("userline: "..userline) - user = getentry(options["users"], userline) - setToken("user", user) - debug("setToken for user: "..user) - state["user"] = user - state["cart"] = {} - end - -- Generate session id - if state["sessionid"] == 0 then - state["sessionid"] = sessionid() - setToken("session", state["sessionid"]) - end - --output event 1 - sendevent(0) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - state["stage"] = state["stage"] + 1 - checkforabort() - else if state["stage"] == 1 then - debug("stage 1") - sendevent(1) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - state["stage"] = state["stage"] + 1 - checkforabort() - else if state["stage"] == 2 then - debug("stage 2") - sendevent(2) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - state["stage"] = state["stage"] + 1 - state["cartrepeats"] = state["cartrepeats"] + 1 - checkforabort() - else if state["stage"] == 3 then - debug("stage 3") - sendevent(3) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - state["stage"] = state["stage"] + 1 - checkforabort() - else if state["stage"] == 4 then - debug("stage 4") - local itemcode = getitemcodechoice() - debug(itemcode) - setToken("itemcode", itemcode) - sendevent(4) - setcountdown() - state["cartitemcount"] = state["cartitemcount"] + 1 - table.insert(state["cart"], itemcode) - state["traversalsteps"] = state["traversalsteps"] + 1 - if state["cartrepeats"] >= state["maxcartrepeats"] then - -- no more cart loops - state["stage"] = state["stage"] + 1 - else - -- cart loop - state["stage"] = 2 - end - checkforabort() - else if state["stage"] == 5 then - debug("stage 5") - -- skip checkout if no items in cart - if state["itemcount"] == 0 then - state["stage"] = 7 - setcountdown() - checkforabort() - else - setToken("itemcount", state["cartitemcount"]) - sendevent(5) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - state["stage"] = state["stage"] + 1 - checkforabort() - end - else if state["stage"] == 6 then - debug("stage 6") - sendevent(6) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - state["stage"] = state["stage"] + 1 - checkforabort() - else if state["stage"] == 7 then - debug("stage 7") - sendevent(7) - setcountdown() - state["traversalsteps"] = state["traversalsteps"] + 1 - reset() - end end end end end end end end -else - -- countdown decrementer - state["countdown"] = state["countdown"] - 1 -end \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/samples/external_ips.sample b/splunk_app_gogen/gogen_assets/samples/external_ips.sample deleted file mode 100644 index 2c7ed05..0000000 --- a/splunk_app_gogen/gogen_assets/samples/external_ips.sample +++ /dev/null @@ -1,150 +0,0 @@ -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -12.130.60.4 -12.130.60.5 -125.17.14.100 -128.241.220.82 -130.253.37.97 -131.178.233.243 -141.146.8.66 -142.162.221.28 -142.233.200.21 -194.215.205.19 -201.122.42.235 -201.28.109.162 -201.3.120.132 -201.42.223.29 -203.92.58.136 -212.235.92.150 -212.27.63.151 -217.132.169.69 -59.162.167.100 -74.125.19.106 -81.11.191.113 -82.245.228.36 -84.34.159.23 -86.212.199.60 -86.9.190.90 -87.194.216.51 -89.167.143.32 -90.205.111.169 -92.1.170.135 -1.16.0.0 -1.19.11.11 -27.1.0.0 -27.1.11.11 -27.35.0.0 -27.35.11.11 -27.96.128.0 -27.96.191.11 -27.101.0.0 -27.101.11.11 -27.102.0.0 -27.102.11.11 -27.160.0.0 -27.175.11.11 -27.176.0.0 -193.33.170.23 -194.146.236.22 -194.8.74.23 -195.216.243.24 -195.69.160.22 -195.69.252.22 -195.80.144.22 -200.6.134.23 -202.164.25.24 -203.223.0.20 -217.197.192.20 -62.216.64.19 -64.66.0.20 -69.80.0.18 -87.240.128.18 -89.11.192.18 -91.199.80.24 -91.205.40.22 -91.208.184.24 -91.214.92.22 -94.229.0.20 -94.229.0.21 \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/samples/itemcode.sample b/splunk_app_gogen/gogen_assets/samples/itemcode.sample deleted file mode 100644 index 896ddd8..0000000 --- a/splunk_app_gogen/gogen_assets/samples/itemcode.sample +++ /dev/null @@ -1,8 +0,0 @@ -1 -2 -3 -4 -5 -6 -7 -8 \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/samples/paymenttype.sample b/splunk_app_gogen/gogen_assets/samples/paymenttype.sample deleted file mode 100644 index bea21a0..0000000 --- a/splunk_app_gogen/gogen_assets/samples/paymenttype.sample +++ /dev/null @@ -1,3 +0,0 @@ -credit -debit -loyalty points \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/samples/useragents.sample b/splunk_app_gogen/gogen_assets/samples/useragents.sample deleted file mode 100644 index 5f02147..0000000 --- a/splunk_app_gogen/gogen_assets/samples/useragents.sample +++ /dev/null @@ -1,84 +0,0 @@ -Mozilla/5.0 (Linux; U; Android 2.3.7; fr-fr; Nexus S Build/GRK39F) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPad; U; CPU OS 4_3_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8G4 Safari/6533.18.5 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0_1 like Mac OS X; en_US) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.3;FBBV/4030.0;FBDV/iPhone3,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/5.0.1;FBSS/2; FBCR/Pelephone;FBID/phone;FBLC/en_US;FBSF/2.0] -Mozilla/5.0 (Linux; U; Android 2.3.3; nb-no; HTC_DesireHD_A9191 Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.5; zh-cn; MI-ONE Plus Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; DROID BIONIC Build/5.5.1_84_DBN-62_MR-11) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0_1 like Mac OS X; en_US) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.3;FBBV/4030.0;FBDV/iPhone3,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/5.0.1;FBSS/2; FBCR/vodafoneUK;FBID/phone;FBLC/en_US;FBSF/2.0] -Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) 1Password/3.6.1/361009 (like Mobile/8C148 Safari/6533.18.5) -Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3 -Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; ADR6400L Build/FRG83D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; ADR6350 Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0_1 like Mac OS X; en_US) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.2;FBBV/4020.0;FBDV/iPhone3,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/5.0.1;FBSS/2; FBCR/AT&T;FBID/phone;FBLC/en_US;FBSF/2.0] -Mozilla/5.0 (Linux; U; Android 2.2.3; en-us; Droid Build/FRK76) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A405 -Mozilla/5.0 (iPad; U; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16 -BlackBerry9300/5.0.0.955 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/102 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5 -Mozilla/5.0 (Linux; U; Android 3.1; de-de; GT-P7510 Build/HMJ37) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13 -Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J3 Safari/6533.18.5 -Mozilla/5.0 (iPad; U; CPU OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_6 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8E200 Safari/6533.18.5 -Mozilla/5.0 (Linux; U; Android 2.3.6; en-us; Nexus One Build/GRK39F) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPad; U; CPU iPhone OS 5_0_1 like Mac OS X; en_US) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.3;FBBV/4030.0;FBDV/iPad1,1;FBMD/iPad;FBSN/iPhone OS;FBSV/5.0.1;FBSS/1; FBCR/;FBID/tablet;FBLC/en_US;FBSF/1.0] -Mozilla/5.0 (Linux; U; Android 2.3.3; en-ca; SAMSUNG-SGH-I997R Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0.261 Mobile Safari/534.11+ -Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; de-de) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 -Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3 -Mozilla/5.0 (iPod; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; es-es) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5 -Mozilla/5.0 (iPhone; CPU iPhone OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Mobile/9A405 -Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-gb) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 -Mozilla/5.0 (Linux; U; Android 2.1-update1; en-us; HUAWEI-M860 Build/ERE27) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17 -Mozilla/5.0 (Android; Linux armv7l; rv:8.0) Gecko/20111104 Firefox/8.0 Fennec/8.0 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; DROID3 Build/5.5.1_84_D3G-55) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 3.2.1; en-us; Transformer TF101 Build/HTK75) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_7 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8E303 Safari/6533.18.5 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_3 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7E18 -Mozilla/5.0 (BlackBerry; U; BlackBerry 9850; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0.374 Mobile Safari/534.11+ -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; T-Mobile G2 Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.3; en-us; DROIDX Build/4.5.1_57_DX5-35) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.3; en-us; Sprint APA9292KT Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_10 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8E600 Safari/6533.18.5 -Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; LG-MS690 Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; ADR6300 Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Incredible 2 Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (BlackBerry; U; BlackBerry 9700; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.526 Mobile Safari/534.8+ -Mozilla/5.0 (Linux; U; Android 2.3.3; ro-ro; GT-I9000 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_3 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7E18 Safari/528.16 -Mozilla/5.0 (Linux; U; Android 2.2; en-us; Comet Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; DROID BIONIC 4G Build/5.5.1_84_DBN-55) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; SPH-M930BST Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -BlackBerry9000/4.6.0.297 Profile/MIDP-2.0 Configuration/CLDC-1.1 VendorID/102 -Mozilla/5.0 (Linux; U; Android 2.3.5; en-us; SAMSUNG-SGH-I927 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3 -Mozilla/5.0 (iPad; U; CPU iPhone OS 5_0_1 like Mac OS X; zh_TW) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.3;FBBV/4030.0;FBDV/iPad2,1;FBMD/iPad;FBSN/iPhone OS;FBSV/5.0.1;FBSS/1; FBCR/;FBID/tablet;FBLC/zh_TW;FBSF/1.0] -BlackBerry9650/5.0.0.1006 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/105 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0_1 like Mac OS X; ja_JP) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.3;FBBV/4030.0;FBDV/iPhone3,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/5.0.1;FBSS/2; FBCR/ -Mozilla/5.0 (iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10 -Mozilla/5.0 (BlackBerry; U; BlackBerry 9300; en-GB) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.600 Mobile Safari/534.8+ -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Mobile/8J2 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0_1 like Mac OS X; ja_JP) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.2;FBBV/4020.0;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/5.0.1;FBSS/2; FBCR/ -Mozilla/5.0 (Linux; U; Android 2.3.3; nl-nl; HTC_DesireHD_A9191 Build/GRI40) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 -Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; SonyEricssonLT15i Build/4.0.2.A.0.42) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Sprint APX515CKT Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Opera/9.80 (Android; Opera Mini/6.5.27452/26.1235; U; en) Presto/2.8.119 Version/10.54 -Mozilla/5.0 (Linux; U; Android 2.3.4; ja-jp; SonyEricssonSO-03C Build/4.0.1.C.1.9) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; GT-I9100 Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_5 like Mac OS X; en-gb) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5 -Mozilla/5.0 (Linux; U; Android 2.1-update1; en-gb; Milestone Build/SHOLS_U2_02.36.0) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17 -Mozilla/5.0 (Linux; U; Android 2.2; en-gb; GT-I9000 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.5; ja-jp; SC-02C Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 2.3.4; ko-kr; HTC_X515E Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_1 like Mac OS X; en-us) AppleWebKit/532.9 (KHTML, like Gecko) Version/4.0.5 Mobile/8B117 Safari/6531.22.7 (compatible; Googlebot-Mobile/2.1; +http://www.google.com/bot.html) -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; ja_JP) AppleWebKit (KHTML, like Gecko) Mobile [FBAN/FBForIPhone;FBAV/4.0.3;FBBV/4030.0;FBDV/iPhone2,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/4.3.3;FBSS/1; FBCR/??????????;FBID/phone;FBLC/ja_JP;FBSF/1.0] -Mozilla/5.0 (Linux; U; Android 2.3.6; en-gb; Nexus One Build/GRK39F) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7E18 -Mozilla/5.0 (Linux; U; Android 2.3.3; en-us; GT-I9100 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 -Mozilla/5.0 (Linux; U; Android 3.2.1; en-us; Xoom Build/HTK75D) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13 -BlackBerry8520/5.0.0.681 Profile/MIDP-2.1 Configuration/CLDC-1.1 VendorID/120 -Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_4 like Mac OS X; fr-fr) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8K2 Safari/6533.18.5 \ No newline at end of file diff --git a/splunk_app_gogen/gogen_assets/samples/webhosts.csv b/splunk_app_gogen/gogen_assets/samples/webhosts.csv deleted file mode 100644 index d19f845..0000000 --- a/splunk_app_gogen/gogen_assets/samples/webhosts.csv +++ /dev/null @@ -1,4 +0,0 @@ -ip,host -10.2.1.33,web-01.bar.com -10.2.1.34,web-02.bar.com -10.2.1.35,web-03.bar.com \ No newline at end of file diff --git a/splunk_app_gogen/local/inputs.conf b/splunk_app_gogen/local/inputs.conf deleted file mode 100644 index 653abad..0000000 --- a/splunk_app_gogen/local/inputs.conf +++ /dev/null @@ -1,15 +0,0 @@ -[gogen://retail_transaction] -config = retail_transaction.yml -config_type = local_file -generator_threads = 3 -disabled = 1 - -[gogen://geopath] -config = geopath.yml -config_type = local_file -disabled = 1 - -[gogen://trig_metrics] -config = trig_metrics.yml -config_type = local_file -disabled = 0 diff --git a/splunk_app_gogen/metadata/default.meta b/splunk_app_gogen/metadata/default.meta deleted file mode 100644 index a9c0b64..0000000 --- a/splunk_app_gogen/metadata/default.meta +++ /dev/null @@ -1,2 +0,0 @@ -[] -export = system \ No newline at end of file diff --git a/splunk_app_gogen/static/appIcon.png b/splunk_app_gogen/static/appIcon.png deleted file mode 100644 index 67a412136bc4d3229ca451eda24c5aecb0a0c8c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 811 zcmV+`1JwM9P)29WRElw%Pyy02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00NUqL_t(o!|m2PPa5*K}V^v0i(^fCbmYcEHos>y?EIeK|$js zh`&VzzcSbILjAOrGzXbCMByd%vi>p)l~qLI>#}9i)SFkPiCSL2Z9NaJxm^ zZVUCg%H?H+ySo|y7K?#Ezy`qSX^Cp}>G@O!2nOwlqJdJWPA*r*$M*;Vw70(}ojyU9MPjiYa=9|9suGF# zNu`b`6sky)iMhExuCJ?HUq9kj z>FMbp9F|)_fq<1zNMd>Un2U?bi(89CeB^QutgQS5AQ}x4iTGJu{K52efI^`{Ds}v# z$Kz=SU~Mhi@>v>P_5SK=rC(dotV={(%&x;3Ux9t&`BsHvAul>z}8lQzP=8^VK;Vr z%Z^~Rw&8S|aXQ78TqV)2ogna==-qLF0sD~i%UQ5pagi#68Q&ln%?(hP$B pldpqxkPgy8I!Fio=b$$izW^jA=OdD5b0h!&002ovPDHLkV1jV6U9kWF diff --git a/splunk_app_gogen/static/appIconAlt.png b/splunk_app_gogen/static/appIconAlt.png deleted file mode 100644 index 67a412136bc4d3229ca451eda24c5aecb0a0c8c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 811 zcmV+`1JwM9P)29WRElw%Pyy02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00NUqL_t(o!|m2PPa5*K}V^v0i(^fCbmYcEHos>y?EIeK|$js zh`&VzzcSbILjAOrGzXbCMByd%vi>p)l~qLI>#}9i)SFkPiCSL2Z9NaJxm^ zZVUCg%H?H+ySo|y7K?#Ezy`qSX^Cp}>G@O!2nOwlqJdJWPA*r*$M*;Vw70(}ojyU9MPjiYa=9|9suGF# zNu`b`6sky)iMhExuCJ?HUq9kj z>FMbp9F|)_fq<1zNMd>Un2U?bi(89CeB^QutgQS5AQ}x4iTGJu{K52efI^`{Ds}v# z$Kz=SU~Mhi@>v>P_5SK=rC(dotV={(%&x;3Ux9t&`BsHvAul>z}8lQzP=8^VK;Vr z%Z^~Rw&8S|aXQ78TqV)2ogna==-qLF0sD~i%UQ5pagi#68Q&ln%?(hP$B pldpqxkPgy8I!Fio=b$$izW^jA=OdD5b0h!&002ovPDHLkV1jV6U9kWF diff --git a/splunk_app_gogen/static/appIconAlt_2x.png b/splunk_app_gogen/static/appIconAlt_2x.png deleted file mode 100644 index 9f41d998da6ec042098a4f1ae67cf3f0540283e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1145 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPN4+pQT@XC9qVu3=EC9V-A&iT2ysd*&~&PAz-C8;S2 z<(VZJ3hti10pX2&;tUMT4?SHRLn>~)y>&Y8a+C<$gZpJkyE*yFcrvy5wuCI~TGbrH z*vcr>%qPe^#mQ0h{J|N9?+*xwa%lPdVo-2c!Px1orIwl5wQ9n%DEU`0QtIgsnxh|2 zQ88P*_-nsTb*}#N&)>?FT4&DjDRZzi(Ge67U=)O*K#qd!q?xc+mSJ+nUoBz0qN!Id_vr-|KvNGfC zy1vH>UCtdYZ3?HGcrz*@T$BviJU`t&=Ax8VYa@5;Qjplrn7hU69fi3*Mnudwu-H9* zLMr3s%ZV&Zb!X13sQ6iQ zyHsCa(3S7==j9(iJhWPM)waC5L`N<9cK&^_Usj$bQk%Bf*Q~JWtN#=6F6#BN>9=!o zXKpr-`11JK*+LekHM=F3Xf$n&)%jNbov&|Y$eiR=SN|RjoK)ezmoGY@+P3WE6E-n1 zHGxhZ$3T`dXM2+iKfZq8pmY1bYw7M8e(j7GUT!*HS+{;Ko9E`62Pdmb?U_HC*6@!wf{wr#UllM9zQzI^%N*zCJsKFyjt*Vd+fUTfl)@7I0U z1GGY$*3H)b^CX4i`ntcYk>8pV&s;Csz9dNX%IjnG5?p#Q0g4RY}{rG72oZYUw!i*>Qq)qgg)1>lo#w?@#55LXYQ zPrsP4<>6<;2bFv>>|3I8|NOYbP0l+XkK))D%B diff --git a/splunk_app_gogen/static/appIcon_2x .png b/splunk_app_gogen/static/appIcon_2x .png deleted file mode 100644 index 9f41d998da6ec042098a4f1ae67cf3f0540283e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1145 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPN4+pQT@XC9qVu3=EC9V-A&iT2ysd*&~&PAz-C8;S2 z<(VZJ3hti10pX2&;tUMT4?SHRLn>~)y>&Y8a+C<$gZpJkyE*yFcrvy5wuCI~TGbrH z*vcr>%qPe^#mQ0h{J|N9?+*xwa%lPdVo-2c!Px1orIwl5wQ9n%DEU`0QtIgsnxh|2 zQ88P*_-nsTb*}#N&)>?FT4&DjDRZzi(Ge67U=)O*K#qd!q?xc+mSJ+nUoBz0qN!Id_vr-|KvNGfC zy1vH>UCtdYZ3?HGcrz*@T$BviJU`t&=Ax8VYa@5;Qjplrn7hU69fi3*Mnudwu-H9* zLMr3s%ZV&Zb!X13sQ6iQ zyHsCa(3S7==j9(iJhWPM)waC5L`N<9cK&^_Usj$bQk%Bf*Q~JWtN#=6F6#BN>9=!o zXKpr-`11JK*+LekHM=F3Xf$n&)%jNbov&|Y$eiR=SN|RjoK)ezmoGY@+P3WE6E-n1 zHGxhZ$3T`dXM2+iKfZq8pmY1bYw7M8e(j7GUT!*HS+{;Ko9E`62Pdmb?U_HC*6@!wf{wr#UllM9zQzI^%N*zCJsKFyjt*Vd+fUTfl)@7I0U z1GGY$*3H)b^CX4i`ntcYk>8pV&s;Csz9dNX%IjnG5?p#Q0g4RY}{rG72oZYUw!i*>Qq)qgg)1>lo#w?@#55LXYQ zPrsP4<>6<;2bFv>>|3I8|NOYbP0l+XkK))D%B diff --git a/template/template.go b/template/template.go index 50b7677..6ca090c 100644 --- a/template/template.go +++ b/template/template.go @@ -39,30 +39,6 @@ func New(name string, template string) error { a, _ := json.Marshal(v) return string(a) }, - "modinput": func(v interface{}) string { - ret := "" - tv := v.(map[string]string) - if _, ok := tv["_raw"]; ok { - ret += "" + tv["_raw"] + "" - } - if _, ok = tv["_time"]; ok { - ret += "" - } - if _, ok = tv["index"]; ok { - ret += "" + tv["index"] + "" - } - if _, ok = tv["host"]; ok { - ret += "" + tv["host"] + "" - } - if _, ok = tv["source"]; ok { - ret += "" + tv["source"] + "" - } - if _, ok = tv["sourcetype"]; ok { - ret += "" + tv["sourcetype"] + "" - } - ret += "" - return ret - }, "keys": func(m map[string]string) []string { keys := make([]string, len(m)) i := 0 From c81b8b20420f5c07083bd1b7b25d7ccf9ee1b4cd Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Sun, 2 Mar 2025 10:15:57 -0800 Subject: [PATCH 04/51] Removed modinput and Splunk app support (#48) --- Makefile | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Makefile b/Makefile index c8543eb..97ffbc4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ GITHUB_OAUTH_CLIENT_ID = 39c483e563cd5cedf7c1 GITHUB_OAUTH_CLIENT_SECRET = 024b16270452504c35f541aca4bf78781cd06db9 -APP_NAME = $(shell basename $(GOGEN_EMBED)) FLAGS = -ldflags "-X github.com/coccyx/gogen/internal.gitHubClientID=$(GITHUB_OAUTH_CLIENT_ID) -X github.com/coccyx/gogen/internal.gitHubClientSecret=$(GITHUB_OAUTH_CLIENT_SECRET) -X main.Version=$(VERSION) -X main.GitSummary=$(SUMMARY) -X main.BuildDate=$(DATE)" GOBIN ?= $(HOME)/go/bin VERSION = $(shell cat $(CURDIR)/VERSION) @@ -37,23 +36,3 @@ test: docker: $(dockercmd) -splunkapp: - tar cfz splunk_app_gogen.spl splunk_app_gogen - -embed: - @if [ -z $$GOGEN_EMBED ]; then \ - echo "Set GOGEN_EMBED to the directory of your Splunk app to embed into."; \ - exit 1; \ - fi - - mkdir -p $$GOGEN_EMBED/bin - cp splunk_app_gogen/bin/gogen.py $$GOGEN_EMBED/bin/$(APP_NAME)_gogen.py - sed -i 's%Gogen%$(APP_NAME) Gogen%' $$GOGEN_EMBED/bin/$(APP_NAME)_gogen.py - sed -i 's%Generate data using Gogen%Generate data using $(APP_NAME) Gogen%' $$GOGEN_EMBED/bin/$(APP_NAME)_gogen.py - mkdir -p $$GOGEN_EMBED/default/data/ui/manager - cp splunk_app_gogen/default/data/ui/manager/gogen_manager.xml $$GOGEN_EMBED/default/data/ui/manager/$(APP_NAME)_gogen_manager.xml - sed -i 's%name="data/inputs/gogen"%name="data/inputs/$(APP_NAME)_gogen"%' $$GOGEN_EMBED/default/data/ui/manager/$(APP_NAME)_gogen_manager.xml - sed -i 's%
Gogen
%
$(APP_NAME) Gogen
%' $$GOGEN_EMBED/default/data/ui/manager/$(APP_NAME)_gogen_manager.xml - sed -i 's%Gogen%$(APP_NAME) Gogen%' $$GOGEN_EMBED/default/data/ui/manager/$(APP_NAME)_gogen_manager.xml - cat splunk_app_gogen/README/inputs.conf.spec >> $$GOGEN_EMBED/README/inputs.conf.spec - sed -i 's%\[gogen://\]%[$(APP_NAME)_gogen://]%' $$GOGEN_EMBED/README/inputs.conf.spec From 5ecb7ea3025de89c4fb357bbd0eabcc7055781db Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Sun, 2 Mar 2025 10:52:33 -0800 Subject: [PATCH 05/51] Adding fix for long intervals not shutting down in a reasonable time (#49) --- timer/timer.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/timer/timer.go b/timer/timer.go index 022b9f2..26ad030 100644 --- a/timer/timer.go +++ b/timer/timer.go @@ -58,9 +58,28 @@ func (t *Timer) NewTimer(cacheIntervals int) { t.cur = 0 } } else { - timer := time.NewTimer(time.Duration(s.Interval) * time.Second) - <-timer.C - t.genWork() + if s.Interval > 5 { + // For longer intervals, use ticker to check closed status + mainTimer := time.NewTimer(time.Duration(s.Interval) * time.Second) + checkTicker := time.NewTicker(1 * time.Second) + select { + case <-mainTimer.C: + checkTicker.Stop() + t.genWork() + case <-checkTicker.C: + if t.closed { + mainTimer.Stop() + checkTicker.Stop() + break + } + continue + } + } else { + // For short intervals, just use the timer directly + timer := time.NewTimer(time.Duration(s.Interval) * time.Second) + <-timer.C + t.genWork() + } } if t.closed { break @@ -113,6 +132,7 @@ Loop1: break Loop1 case <-time.After(1 * time.Second): if t.closed { + log.Debugf("Timer %s closed", t.S.Name) break Loop1 } continue From 7ac15435c5aae09697a167342e2688111d014832 Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Sun, 2 Mar 2025 11:22:36 -0800 Subject: [PATCH 06/51] adding LLM context generator script (#50) --- .github/workflows/ci.yml | 2 + llmcontext/bundle_examples.py | 167 +++++ llmcontext/gogen_examples_bundle.json | 867 ++++++++++++++++++++++++++ llmcontext/requirements.txt | 2 + llmcontext/setup_venv.sh | 23 + 5 files changed, 1061 insertions(+) create mode 100755 llmcontext/bundle_examples.py create mode 100644 llmcontext/gogen_examples_bundle.json create mode 100644 llmcontext/requirements.txt create mode 100755 llmcontext/setup_venv.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be1746e..2953ec6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,11 @@ jobs: $HOME/gopath/bin/goveralls -v -service=github - name: Build Project + if: github.ref == 'refs/heads/master' run: make GOBIN=$HOME/gopath/bin build - name: Build Docker Image + if: github.ref == 'refs/heads/master' run: docker build -t clintsharp/gogen . # Deployment: These steps run only on the main branch. diff --git a/llmcontext/bundle_examples.py b/llmcontext/bundle_examples.py new file mode 100755 index 0000000..a01a62a --- /dev/null +++ b/llmcontext/bundle_examples.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +import os +import yaml +import json +import markdown +import datetime +from pathlib import Path +from typing import Dict, List, Any +import re + +class DateTimeEncoder(json.JSONEncoder): + """Custom JSON encoder for handling datetime objects.""" + def default(self, obj): + if isinstance(obj, datetime.datetime): + return obj.isoformat() + return super().default(obj) + +def clean_markdown(md_content: str) -> str: + """Convert markdown to plain text while preserving structure.""" + # Remove code blocks but keep their content + md_content = re.sub(r'```[^\n]*\n', '', md_content) + md_content = re.sub(r'```', '', md_content) + + # Convert headers to plain text with newlines + md_content = re.sub(r'^#{1,6}\s+(.+)$', r'\1\n', md_content, flags=re.MULTILINE) + + return md_content.strip() + +def extract_yaml_comments(yaml_content: str) -> Dict[str, str]: + """Extract inline comments from YAML content.""" + comments = {} + for line_num, line in enumerate(yaml_content.split('\n'), 1): + if '#' in line: + code, comment = line.split('#', 1) + if code.strip(): # Only store comments for lines with actual code + comments[line_num] = comment.strip() + return comments + +def read_yaml_file(file_path: str) -> Dict[str, Any]: + """Read and parse a YAML file with error handling.""" + try: + with open(file_path, 'r') as f: + content = f.read() + # Replace tabs with spaces to avoid YAML parsing errors + content = content.replace('\t', ' ') + yaml_content = yaml.safe_load(content) + comments = extract_yaml_comments(content) + return { + 'content': yaml_content, + 'raw_content': content, + 'comments': comments + } + except Exception as e: + print(f"Warning: Error reading YAML file {file_path}: {str(e)}") + # Return partial data even if YAML parsing fails + return { + 'content': None, + 'raw_content': content if 'content' in locals() else None, + 'comments': extract_yaml_comments(content) if 'content' in locals() else {}, + 'error': str(e) + } + +def read_markdown_file(file_path: str) -> str: + """Read and process a markdown file.""" + try: + with open(file_path, 'r') as f: + content = f.read() + return clean_markdown(content) + except Exception as e: + print(f"Error reading markdown file {file_path}: {str(e)}") + return "" + +def get_category_from_path(file_path: str) -> str: + """Extract category from file path.""" + parts = Path(file_path).parts + if 'examples' in parts: + idx = parts.index('examples') + if len(parts) > idx + 1: + return parts[idx + 1] + return "uncategorized" + +def process_examples(base_path: str) -> List[Dict[str, Any]]: + """Process all YAML examples in the codebase.""" + examples = [] + example_dirs = [ + 'examples/tutorial', + 'examples/weblog', + 'examples/csv', + 'examples/nixOS', + ] + + for dir_path in example_dirs: + full_path = os.path.join(base_path, dir_path) + if not os.path.exists(full_path): + continue + + for root, _, files in os.walk(full_path): + for file in files: + if file.endswith(('.yml', '.yaml')): + file_path = os.path.join(root, file) + yaml_data = read_yaml_file(file_path) + + if yaml_data['content'] is not None: + example = { + 'name': file, + 'category': get_category_from_path(file_path), + 'yaml_content': yaml_data['raw_content'], + 'parsed_content': yaml_data['content'], + 'comments': yaml_data['comments'], + 'file_path': os.path.relpath(file_path, base_path) + } + examples.append(example) + + return examples + +def process_documentation(base_path: str) -> Dict[str, str]: + """Process all relevant documentation files.""" + docs = {} + doc_files = { + 'reference': 'README/Reference.md', + 'tutorial': 'README/Tutorial.md', + 'examples': 'README/Examples.md' + } + + for doc_type, file_path in doc_files.items(): + full_path = os.path.join(base_path, file_path) + if os.path.exists(full_path): + docs[doc_type] = read_markdown_file(full_path) + + return docs + +def main(): + # Use current directory as base path + base_path = os.getcwd() + + # Process examples and documentation + examples = process_examples(base_path) + documentation = process_documentation(base_path) + + # Create final structure + bundle = { + 'examples': examples, + 'documentation': documentation, + 'metadata': { + 'total_examples': len(examples), + 'documentation_sections': list(documentation.keys()), + 'generated_at': datetime.datetime.now() # No need to call isoformat() here + } + } + + # Write to file + output_file = 'gogen_examples_bundle.json' + with open(output_file, 'w') as f: + json.dump(bundle, f, indent=2, cls=DateTimeEncoder) + + print(f"Successfully bundled {len(examples)} examples and {len(documentation)} documentation sections to {output_file}") + + # Print any examples that had parsing errors + errors = [ex for ex in examples if ex.get('parsed_content') is None] + if errors: + print("\nWarning: The following files had YAML parsing errors but were included with raw content:") + for ex in errors: + print(f"- {ex['file_path']}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/llmcontext/gogen_examples_bundle.json b/llmcontext/gogen_examples_bundle.json new file mode 100644 index 0000000..b484cbc --- /dev/null +++ b/llmcontext/gogen_examples_bundle.json @@ -0,0 +1,867 @@ +{ + "examples": [ + { + "name": "tutorial2.yml", + "category": "tutorial", + "yaml_content": "raters:\n - name: eventrater\n type: config\n options:\n MinuteOfHour:\n 0: 1.0\n 1: 0.5\n 2: 2.0\n - name: valrater\n type: script\n script: >\n return options[\"multiplier\"]\n options:\n multiplier: 2\n\nsamples:\n - name: tutorial2\n description: Tutorial 2\n begin: 2012-02-09T08:00:00Z\n end: 2012-02-09T08:03:00Z\n interval: 60\n count: 2\n rater: eventrater\n\n tokens:\n - name: ts\n format: template \n type: timestamp\n replacement: \"%b/%d/%y %H:%M:%S\"\n - name: linenum\n format: template\n type: script\n init:\n id: \"0\"\n script: >\n state[\"id\"] = state[\"id\"] + 1\n return state[\"id\"]\n - name: val\n format: template\n type: random\n replacement: int\n lower: 1\n upper: 5\n - name: rated\n format: template\n type: rated\n replacement: int\n lower: 1\n upper: 3\n rater: valrater\n\n lines:\n - _raw: $ts$ line=$linenum$ value=$val$ rated=$rated$", + "parsed_content": { + "raters": [ + { + "name": "eventrater", + "type": "config", + "options": { + "MinuteOfHour": { + "0": 1.0, + "1": 0.5, + "2": 2.0 + } + } + }, + { + "name": "valrater", + "type": "script", + "script": "return options[\"multiplier\"]\n", + "options": { + "multiplier": 2 + } + } + ], + "samples": [ + { + "name": "tutorial2", + "description": "Tutorial 2", + "begin": "2012-02-09T08:00:00+00:00", + "end": "2012-02-09T08:03:00+00:00", + "interval": 60, + "count": 2, + "rater": "eventrater", + "tokens": [ + { + "name": "ts", + "format": "template", + "type": "timestamp", + "replacement": "%b/%d/%y %H:%M:%S" + }, + { + "name": "linenum", + "format": "template", + "type": "script", + "init": { + "id": "0" + }, + "script": "state[\"id\"] = state[\"id\"] + 1 return state[\"id\"]\n" + }, + { + "name": "val", + "format": "template", + "type": "random", + "replacement": "int", + "lower": 1, + "upper": 5 + }, + { + "name": "rated", + "format": "template", + "type": "rated", + "replacement": "int", + "lower": 1, + "upper": 3, + "rater": "valrater" + } + ], + "lines": [ + { + "_raw": "$ts$ line=$linenum$ value=$val$ rated=$rated$" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/tutorial/tutorial2.yml" + }, + { + "name": "tutorial6.yml", + "category": "tutorial", + "yaml_content": "mix:\n - sample: $GOGEN_HOME/examples/tutorial/tutorial1.yml\n begin: now\n realtime: true\n count: 1\n interval: 1\n - sample: $GOGEN_HOME/examples/tutorial/tutorial2.yml\n begin: now\n realtime: true\n count: 1\n interval: 1\n - sample: $GOGEN_HOME/examples/tutorial/tutorial5.yml\n begin: now\n realtime: true", + "parsed_content": { + "mix": [ + { + "sample": "$GOGEN_HOME/examples/tutorial/tutorial1.yml", + "begin": "now", + "realtime": true, + "count": 1, + "interval": 1 + }, + { + "sample": "$GOGEN_HOME/examples/tutorial/tutorial2.yml", + "begin": "now", + "realtime": true, + "count": 1, + "interval": 1 + }, + { + "sample": "$GOGEN_HOME/examples/tutorial/tutorial5.yml", + "begin": "now", + "realtime": true + } + ] + }, + "comments": {}, + "file_path": "examples/tutorial/tutorial6.yml" + }, + { + "name": "tutorial1.yml", + "category": "tutorial", + "yaml_content": "samples:\n - name: tutorial1\n description: Tutorial 1\n interval: 1\n endIntervals: 5\n count: 1\n randomizeEvents: true\n \n tokens:\n - name: ts\n format: template \n type: timestamp\n replacement: \"%b/%d/%y %H:%M:%S\"\n\n lines:\n - _raw: $ts$ line1\n - _raw: $ts$ line2\n - _raw: $ts$ line3", + "parsed_content": { + "samples": [ + { + "name": "tutorial1", + "description": "Tutorial 1", + "interval": 1, + "endIntervals": 5, + "count": 1, + "randomizeEvents": true, + "tokens": [ + { + "name": "ts", + "format": "template", + "type": "timestamp", + "replacement": "%b/%d/%y %H:%M:%S" + } + ], + "lines": [ + { + "_raw": "$ts$ line1" + }, + { + "_raw": "$ts$ line2" + }, + { + "_raw": "$ts$ line3" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/tutorial/tutorial1.yml" + }, + { + "name": "tutorial5.yml", + "category": "tutorial", + "yaml_content": "samples: \n- name: tutorial5\n generator: replay\n\n tokens:\n - name: ts1\n type: timestamp\n format: regex\n token: (\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2})\n replacement: '%d/%b/%Y:%H:%M:%S'\n fromSample: results.csv", + "parsed_content": { + "samples": [ + { + "name": "tutorial5", + "generator": "replay", + "tokens": [ + { + "name": "ts1", + "type": "timestamp", + "format": "regex", + "token": "(\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2})", + "replacement": "%d/%b/%Y:%H:%M:%S" + } + ], + "fromSample": "results.csv" + } + ] + }, + "comments": {}, + "file_path": "examples/tutorial/tutorial5.yml" + }, + { + "name": "tutorial4.yml", + "category": "tutorial", + "yaml_content": "global:\n output:\n outputTemplate: inifile\n outputter: stdout\n\ngenerators:\n- name: indexes.conf\n script: |\n header = \"# Generated at $ts$\"\n\n events = { }\n for i=0,2 do\n line = getLine(i)\n line[\"header\"] = header\n line = replaceTokens(line)\n\n if line[\"maxHotSpanSecs\"] ~= nil then\n frozenTimePeriodInSecs = tonumber(line[\"maxHotSpanSecs\"]) * 6\n line[\"frozenTimePeriodInSecs\"] = tostring(frozenTimePeriodInSecs)\n end\n\n line[\"homePath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/db\"\n line[\"coldPath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/colddb\"\n line[\"thawedPath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/thaweddb\"\n table.insert(events, line)\n end\n send(events)\n\nsamples:\n- name: inifile\n\n generator: indexes.conf\n\n count: 1\n endIntervals: 1\n\n tokens:\n - name: ts\n field: header\n format: template \n type: timestamp\n replacement: \"%b/%d/%y %H:%M:%S\"\n\n lines:\n - index: history\n maxDataSize: 10\n - index: summary\n - index: _internal\n maxDataSize: 1000\n maxHotSpanSecs: 432000\n\ntemplates:\n- name: inifile\n header: '{{ .header }}'\n row: |\n [{{ .index }}]\n homePath = {{ .homePath }}\n coldPath = {{ .coldPath }} \n thawedPath = {{ .thawedPath }}\n {{ if .maxDataSize -}}\n maxDataSize = {{ .maxDataSize }}\n {{ end }}{{ if .maxHotSpanSecs -}}\n maxHotSpanSecs = {{ .maxHotSpanSecs }}\n {{ end }}{{ if .frozenTimePeriodInSecs -}}\n frozenTimePeriodInSecs = {{ .frozenTimePeriodInSecs }}{{ end }}\n", + "parsed_content": { + "global": { + "output": { + "outputTemplate": "inifile", + "outputter": "stdout" + } + }, + "generators": [ + { + "name": "indexes.conf", + "script": "header = \"# Generated at $ts$\"\n\nevents = { }\nfor i=0,2 do\n line = getLine(i)\n line[\"header\"] = header\n line = replaceTokens(line)\n\n if line[\"maxHotSpanSecs\"] ~= nil then\n frozenTimePeriodInSecs = tonumber(line[\"maxHotSpanSecs\"]) * 6\n line[\"frozenTimePeriodInSecs\"] = tostring(frozenTimePeriodInSecs)\n end\n\n line[\"homePath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/db\"\n line[\"coldPath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/colddb\"\n line[\"thawedPath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/thaweddb\"\n table.insert(events, line)\nend\nsend(events)\n" + } + ], + "samples": [ + { + "name": "inifile", + "generator": "indexes.conf", + "count": 1, + "endIntervals": 1, + "tokens": [ + { + "name": "ts", + "field": "header", + "format": "template", + "type": "timestamp", + "replacement": "%b/%d/%y %H:%M:%S" + } + ], + "lines": [ + { + "index": "history", + "maxDataSize": 10 + }, + { + "index": "summary" + }, + { + "index": "_internal", + "maxDataSize": 1000, + "maxHotSpanSecs": 432000 + } + ] + } + ], + "templates": [ + { + "name": "inifile", + "header": "{{ .header }}", + "row": "[{{ .index }}]\nhomePath = {{ .homePath }}\ncoldPath = {{ .coldPath }} \nthawedPath = {{ .thawedPath }}\n{{ if .maxDataSize -}}\nmaxDataSize = {{ .maxDataSize }}\n{{ end }}{{ if .maxHotSpanSecs -}}\nmaxHotSpanSecs = {{ .maxHotSpanSecs }}\n{{ end }}{{ if .frozenTimePeriodInSecs -}}\nfrozenTimePeriodInSecs = {{ .frozenTimePeriodInSecs }}{{ end }}\n" + } + ] + }, + "comments": { + "9": "Generated at $ts$\"" + }, + "file_path": "examples/tutorial/tutorial4.yml" + }, + { + "name": "tutorial3.yml", + "category": "tutorial", + "yaml_content": "name: tutorial3\ndescription: Tutorial 3\nbegin: 2012-02-09T08:00:00Z\nend: 2012-02-09T08:03:00Z\ninterval: 60\ncount: 2\nrater: eventrater\ntokens:\n - name: ts\n format: regex \n token: (\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2},\\d{3})\n type: gotimestamp\n replacement: \"2006-01-02 15:04:05.000\"\n - name: host\n format: template\n type: choice\n field: host\n choice:\n - server1.gogen.io\n - server2.gogen.io\n - name: transtype\n format: regex\n token: transType=(\\w+)\n type: weightedChoice\n weightedChoice:\n - weight: 3\n choice: New\n - weight: 5\n choice: Change\n - weight: 1\n choice: Delete\n - name: integerid\n format: template\n type: script\n init:\n id: \"0\"\n script: >\n state[\"id\"] = state[\"id\"] + 1\n return state[\"id\"]\n - name: guid\n format: template\n type: random\n replacement: guid\n - name: username\n format: template\n type: choice\n sample: usernames.sample\n - name: markets-city \n format: template\n token: $city$\n type: fieldChoice\n sample: markets.csv\n srcField: city\n group: 1\n - name: markets-state \n format: template\n token: $state$\n type: fieldChoice\n sample: markets.csv\n srcField: state\n group: 1\n - name: markets-zip\n format: template\n token: $zip$\n type: fieldChoice\n sample: markets.csv\n srcField: zip\n group: 1\n - name: value\n format: regex\n token: value=(\\d+)\n type: random\n replacement: float\n precision: 3\n lower: 0\n upper: 10\n\nlines:\n - index: main\n host: $host$\n sourcetype: translog\n source: /var/log/translog\n _raw: 2012-09-14 16:30:20,072 transType=ReplaceMe transID=$integerid$ transGUID=$guid$ userName=$username$ city=\"$city$\" state=$state$ zip=$zip$ value=0", + "parsed_content": { + "name": "tutorial3", + "description": "Tutorial 3", + "begin": "2012-02-09T08:00:00+00:00", + "end": "2012-02-09T08:03:00+00:00", + "interval": 60, + "count": 2, + "rater": "eventrater", + "tokens": [ + { + "name": "ts", + "format": "regex", + "token": "(\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2},\\d{3})", + "type": "gotimestamp", + "replacement": "2006-01-02 15:04:05.000" + }, + { + "name": "host", + "format": "template", + "type": "choice", + "field": "host", + "choice": [ + "server1.gogen.io", + "server2.gogen.io" + ] + }, + { + "name": "transtype", + "format": "regex", + "token": "transType=(\\w+)", + "type": "weightedChoice", + "weightedChoice": [ + { + "weight": 3, + "choice": "New" + }, + { + "weight": 5, + "choice": "Change" + }, + { + "weight": 1, + "choice": "Delete" + } + ] + }, + { + "name": "integerid", + "format": "template", + "type": "script", + "init": { + "id": "0" + }, + "script": "state[\"id\"] = state[\"id\"] + 1 return state[\"id\"]\n" + }, + { + "name": "guid", + "format": "template", + "type": "random", + "replacement": "guid" + }, + { + "name": "username", + "format": "template", + "type": "choice", + "sample": "usernames.sample" + }, + { + "name": "markets-city", + "format": "template", + "token": "$city$", + "type": "fieldChoice", + "sample": "markets.csv", + "srcField": "city", + "group": 1 + }, + { + "name": "markets-state", + "format": "template", + "token": "$state$", + "type": "fieldChoice", + "sample": "markets.csv", + "srcField": "state", + "group": 1 + }, + { + "name": "markets-zip", + "format": "template", + "token": "$zip$", + "type": "fieldChoice", + "sample": "markets.csv", + "srcField": "zip", + "group": 1 + }, + { + "name": "value", + "format": "regex", + "token": "value=(\\d+)", + "type": "random", + "replacement": "float", + "precision": 3, + "lower": 0, + "upper": 10 + } + ], + "lines": [ + { + "index": "main", + "host": "$host$", + "sourcetype": "translog", + "source": "/var/log/translog", + "_raw": "2012-09-14 16:30:20,072 transType=ReplaceMe transID=$integerid$ transGUID=$guid$ userName=$username$ city=\"$city$\" state=$state$ zip=$zip$ value=0" + } + ] + }, + "comments": {}, + "file_path": "examples/tutorial/tutorial3/samples/tutorial3.yml" + }, + { + "name": "eventrater.yml", + "category": "tutorial", + "yaml_content": "name: eventrater\ntype: config\noptions:\n MinuteOfHour:\n 0: 1.0\n 1: 0.5\n 2: 2.0", + "parsed_content": { + "name": "eventrater", + "type": "config", + "options": { + "MinuteOfHour": { + "0": 1.0, + "1": 0.5, + "2": 2.0 + } + } + }, + "comments": {}, + "file_path": "examples/tutorial/tutorial3/raters/eventrater.yml" + }, + { + "name": "weblog.yml", + "category": "weblog", + "yaml_content": "global:\n samplesDir: \n - $GOGEN_HOME/examples/common\nsamples:\n - name: weblog\n fromSample: weblog-common\n interval: 1\n endIntervals: 1\n count: 10\n ", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/common" + ] + }, + "samples": [ + { + "name": "weblog", + "fromSample": "weblog-common", + "interval": 1, + "endIntervals": 1, + "count": 10 + } + ] + }, + "comments": {}, + "file_path": "examples/weblog/weblog.yml" + }, + { + "name": "csv.yml", + "category": "csv", + "yaml_content": "global:\n output:\n outputTemplate: csv\nsamples:\n- name: csv\n description: Simple CSV Example\n notes: >\n For CSV OutputTemplate, it's critial to note that it will only work well for one interval, due to the fact that\n the output template will print a header for every interval.\n endIntervals: 1\n count: 100\n tokens:\n - name: transtype # An inline token defined in this YAML\n format: template\n type: weightedChoice\n field: transtype\n weightedChoice:\n - weight: 3\n choice: New\n - weight: 5\n choice: Change\n - weight: 1\n choice: Delete\n - name: usernames\n field: username\n format: template\n token: $username$\n type: choice\n sample: usernames.sample\n - name: markets-city \n format: template\n token: $city$\n type: fieldChoice\n sample: markets.csv\n srcField: city\n field: city\n group: 1\n - name: markets-state \n format: template\n token: $state$\n type: fieldChoice\n sample: markets.csv\n srcField: state\n field: state\n group: 1\n - name: markets-zip\n format: template\n token: $zip$\n type: fieldChoice\n sample: markets.csv\n srcField: zip\n field: zip\n group: 1\n - name: value\n format: template\n field: value\n type: random\n replacement: float\n precision: 3\n lower: 0\n upper: 10\n\n lines:\n - username: $username$\n transtype: $transtype$\n city: $city$\n state: $state$\n zip: $zip$\n value: $value$", + "parsed_content": { + "global": { + "output": { + "outputTemplate": "csv" + } + }, + "samples": [ + { + "name": "csv", + "description": "Simple CSV Example", + "notes": "For CSV OutputTemplate, it's critial to note that it will only work well for one interval, due to the fact that the output template will print a header for every interval.\n", + "endIntervals": 1, + "count": 100, + "tokens": [ + { + "name": "transtype", + "format": "template", + "type": "weightedChoice", + "field": "transtype", + "weightedChoice": [ + { + "weight": 3, + "choice": "New" + }, + { + "weight": 5, + "choice": "Change" + }, + { + "weight": 1, + "choice": "Delete" + } + ] + }, + { + "name": "usernames", + "field": "username", + "format": "template", + "token": "$username$", + "type": "choice", + "sample": "usernames.sample" + }, + { + "name": "markets-city", + "format": "template", + "token": "$city$", + "type": "fieldChoice", + "sample": "markets.csv", + "srcField": "city", + "field": "city", + "group": 1 + }, + { + "name": "markets-state", + "format": "template", + "token": "$state$", + "type": "fieldChoice", + "sample": "markets.csv", + "srcField": "state", + "field": "state", + "group": 1 + }, + { + "name": "markets-zip", + "format": "template", + "token": "$zip$", + "type": "fieldChoice", + "sample": "markets.csv", + "srcField": "zip", + "field": "zip", + "group": 1 + }, + { + "name": "value", + "format": "template", + "field": "value", + "type": "random", + "replacement": "float", + "precision": 3, + "lower": 0, + "upper": 10 + } + ], + "lines": [ + { + "username": "$username$", + "transtype": "$transtype$", + "city": "$city$", + "state": "$state$", + "zip": "$zip$", + "value": "$value$" + } + ] + } + ] + }, + "comments": { + "13": "An inline token defined in this YAML" + }, + "file_path": "examples/csv/csv.yml" + }, + { + "name": "nix.yml", + "category": "nixOS", + "yaml_content": "global:\n samplesDir:\n - $GOGEN_HOME/examples/nixOS\nmix:\n - sample: $GOGEN_HOME/examples/nixOS/cpu.yml\n - sample: $GOGEN_HOME/examples/nixOS/df.yml\n - sample: $GOGEN_HOME/examples/nixOS/vmstat.yml\n - sample: $GOGEN_HOME/examples/nixOS/bandwidth.yml\n - sample: $GOGEN_HOME/examples/nixOS/iostat.yml", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/nixOS" + ] + }, + "mix": [ + { + "sample": "$GOGEN_HOME/examples/nixOS/cpu.yml" + }, + { + "sample": "$GOGEN_HOME/examples/nixOS/df.yml" + }, + { + "sample": "$GOGEN_HOME/examples/nixOS/vmstat.yml" + }, + { + "sample": "$GOGEN_HOME/examples/nixOS/bandwidth.yml" + }, + { + "sample": "$GOGEN_HOME/examples/nixOS/iostat.yml" + } + ] + }, + "comments": {}, + "file_path": "examples/nixOS/nix.yml" + }, + { + "name": "iostat.yml", + "category": "nixOS", + "yaml_content": "global:\n samplesDir:\n - $GOGEN_HOME/examples/nixOS\ngenerators:\n - name: iostat\n fileName: $GOGEN_HOME/examples/nixOS/iostat.lua\n options:\n highWrites: 0\n highReads: 0\n maxOps: 1000\n avgKB: 0.512\n maxTime: 100\nsamples:\n - name: iostat\n description: Generate Iostat Usage Metrics\n notes: >\n Generates iostat usage from the Splunk UNIX TA\n generator: iostat\n interval: 60\n tokens:\n - name: host\n type: fieldChoice\n srcField: host\n sample: allhosts.csv\n disabled: true\n - name: disks\n type: choice\n choice:\n - sda\n - sdb\n - dm-0\n - dm-1\n disabled: true\n lines:\n - index: os\n sourcetype: iostat\n source: iostat\n host: $host$\n _raw: Device rReq_PS wReq_PS rKB_PS wKB_PS avgWaitMillis avgSvcMillis bandwUtilPct\n - index: os\n sourcetype: iostat\n source: iostat\n host: $host$\n _raw: $device$ $rrps$ $wrps$ $rkbps$ $wkbps$ $avgwait$ $avgsvc$ $bwutil$", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/nixOS" + ] + }, + "generators": [ + { + "name": "iostat", + "fileName": "$GOGEN_HOME/examples/nixOS/iostat.lua", + "options": { + "highWrites": 0, + "highReads": 0, + "maxOps": 1000, + "avgKB": 0.512, + "maxTime": 100 + } + } + ], + "samples": [ + { + "name": "iostat", + "description": "Generate Iostat Usage Metrics", + "notes": "Generates iostat usage from the Splunk UNIX TA\n", + "generator": "iostat", + "interval": 60, + "tokens": [ + { + "name": "host", + "type": "fieldChoice", + "srcField": "host", + "sample": "allhosts.csv", + "disabled": true + }, + { + "name": "disks", + "type": "choice", + "choice": [ + "sda", + "sdb", + "dm-0", + "dm-1" + ], + "disabled": true + } + ], + "lines": [ + { + "index": "os", + "sourcetype": "iostat", + "source": "iostat", + "host": "$host$", + "_raw": "Device rReq_PS wReq_PS rKB_PS wKB_PS avgWaitMillis avgSvcMillis bandwUtilPct" + }, + { + "index": "os", + "sourcetype": "iostat", + "source": "iostat", + "host": "$host$", + "_raw": "$device$ $rrps$ $wrps$ $rkbps$ $wkbps$ $avgwait$ $avgsvc$ $bwutil$" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/nixOS/iostat.yml" + }, + { + "name": "df.yml", + "category": "nixOS", + "yaml_content": "global:\n samplesDir:\n - $GOGEN_HOME/examples/nixOS\ngenerators:\n - name: df\n fileName: $GOGEN_HOME/examples/nixOS/df.lua\n options:\n minDiskUsedPct: 50.0\n maxDiskUsedPct: 69.0\n totalGBperDisk: 931\n numDisks: 4\nsamples:\n - name: df\n description: Generate Disk Usage Metrics\n notes: >\n Generates Disk Usage in the form of a df command from the Splunk UNIX TA\n generator: df\n interval: 60\n tokens:\n - name: disks\n type: fieldChoice\n srcField: disk\n sample: disks.csv\n disabled: true\n - name: host\n type: fieldChoice\n srcField: host\n sample: allhosts.csv\n disabled: true\n lines:\n - index: os\n sourcetype: df\n source: df\n host: $host$\n _raw: Filesystem Type Size Used Avail UsePct MountedOn\n - index: os\n sourcetype: df\n source: df\n host: $host$\n _raw: $fs$ ext4 $totalGB$G $usedGB$G $availGB$G $usedPct$% $mnt$", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/nixOS" + ] + }, + "generators": [ + { + "name": "df", + "fileName": "$GOGEN_HOME/examples/nixOS/df.lua", + "options": { + "minDiskUsedPct": 50.0, + "maxDiskUsedPct": 69.0, + "totalGBperDisk": 931, + "numDisks": 4 + } + } + ], + "samples": [ + { + "name": "df", + "description": "Generate Disk Usage Metrics", + "notes": "Generates Disk Usage in the form of a df command from the Splunk UNIX TA\n", + "generator": "df", + "interval": 60, + "tokens": [ + { + "name": "disks", + "type": "fieldChoice", + "srcField": "disk", + "sample": "disks.csv", + "disabled": true + }, + { + "name": "host", + "type": "fieldChoice", + "srcField": "host", + "sample": "allhosts.csv", + "disabled": true + } + ], + "lines": [ + { + "index": "os", + "sourcetype": "df", + "source": "df", + "host": "$host$", + "_raw": "Filesystem Type Size Used Avail UsePct MountedOn" + }, + { + "index": "os", + "sourcetype": "df", + "source": "df", + "host": "$host$", + "_raw": "$fs$ ext4 $totalGB$G $usedGB$G $availGB$G $usedPct$% $mnt$" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/nixOS/df.yml" + }, + { + "name": "vmstat.yml", + "category": "nixOS", + "yaml_content": "global:\n samplesDir:\n - $GOGEN_HOME/examples/nixOS\ngenerators:\n - name: vmstat\n fileName: $GOGEN_HOME/examples/nixOS/vmstat.lua\n options:\n minMemUsedPct: 5.0\n maxMemUsedPct: 60.0\n totalMB: 16000\nsamples:\n - name: vmstat\n description: Generate Memory Usage Metrics\n notes: >\n Generates memory Usage in the form of a vmstat command from the Splunk UNIX TA\n generator: vmstat\n interval: 60\n tokens:\n - name: host\n type: fieldChoice\n srcField: host\n sample: allhosts.csv\n disabled: true\n lines:\n - index: os\n sourcetype: df\n source: df\n host: $host$\n _raw: memTotalMB memFreeMB memUsedMB memFreePct memUsedPct pgPageOut swapUsedPct pgSwapOut cSwitches interrupts forks processes threads loadAvg1mi waitThreads interrupts_PS pgPageIn_PS pgPageOut_PS\n - index: os\n sourcetype: df\n source: df\n host: $host$\n _raw: $memTotalMB$ $memFreeMB$ $memUsedMB$ $memFreePct$ $memUsedPct$ $pgPageOut$ $swapUsedPct$ $pgSwapOut$ $cSwitches$ $interrupts$ $forks$ $processes$ $threads$ $loadAvg1mi$ $waitThreads$ $interruptsPS$ $pgPageInPS$ $pgPageOutPS$", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/nixOS" + ] + }, + "generators": [ + { + "name": "vmstat", + "fileName": "$GOGEN_HOME/examples/nixOS/vmstat.lua", + "options": { + "minMemUsedPct": 5.0, + "maxMemUsedPct": 60.0, + "totalMB": 16000 + } + } + ], + "samples": [ + { + "name": "vmstat", + "description": "Generate Memory Usage Metrics", + "notes": "Generates memory Usage in the form of a vmstat command from the Splunk UNIX TA\n", + "generator": "vmstat", + "interval": 60, + "tokens": [ + { + "name": "host", + "type": "fieldChoice", + "srcField": "host", + "sample": "allhosts.csv", + "disabled": true + } + ], + "lines": [ + { + "index": "os", + "sourcetype": "df", + "source": "df", + "host": "$host$", + "_raw": "memTotalMB memFreeMB memUsedMB memFreePct memUsedPct pgPageOut swapUsedPct pgSwapOut cSwitches interrupts forks processes threads loadAvg1mi waitThreads interrupts_PS pgPageIn_PS pgPageOut_PS" + }, + { + "index": "os", + "sourcetype": "df", + "source": "df", + "host": "$host$", + "_raw": "$memTotalMB$ $memFreeMB$ $memUsedMB$ $memFreePct$ $memUsedPct$ $pgPageOut$ $swapUsedPct$ $pgSwapOut$ $cSwitches$ $interrupts$ $forks$ $processes$ $threads$ $loadAvg1mi$ $waitThreads$ $interruptsPS$ $pgPageInPS$ $pgPageOutPS$" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/nixOS/vmstat.yml" + }, + { + "name": "bandwidth.yml", + "category": "nixOS", + "yaml_content": "global:\n samplesDir:\n - $GOGEN_HOME/examples/nixOS\ngenerators:\n - name: bandwidth\n fileName: $GOGEN_HOME/examples/nixOS/bandwidth.lua\n options:\n minKBPS: 1000\n maxKBPS: 1500\n numNICs: 2\nsamples:\n - name: bandwidth\n description: Generate Bandwidth Usage Metrics\n notes: >\n Generates bandwidth usage from the Splunk UNIX TA\n generator: bandwidth\n interval: 60\n tokens:\n - name: host\n type: fieldChoice\n srcField: host\n sample: allhosts.csv\n disabled: true\n lines:\n - index: os\n sourcetype: bandwidth\n source: bandwidth\n host: $host$\n _raw: Name rxPackets_PS txPackets_PS rxKB_PS txKB_PS\n - index: os\n sourcetype: df\n source: df\n host: $host$\n _raw: $nic$ $rx_p$ $tx_p$ $rx_kb$ $tx_kb$", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/nixOS" + ] + }, + "generators": [ + { + "name": "bandwidth", + "fileName": "$GOGEN_HOME/examples/nixOS/bandwidth.lua", + "options": { + "minKBPS": 1000, + "maxKBPS": 1500, + "numNICs": 2 + } + } + ], + "samples": [ + { + "name": "bandwidth", + "description": "Generate Bandwidth Usage Metrics", + "notes": "Generates bandwidth usage from the Splunk UNIX TA\n", + "generator": "bandwidth", + "interval": 60, + "tokens": [ + { + "name": "host", + "type": "fieldChoice", + "srcField": "host", + "sample": "allhosts.csv", + "disabled": true + } + ], + "lines": [ + { + "index": "os", + "sourcetype": "bandwidth", + "source": "bandwidth", + "host": "$host$", + "_raw": "Name rxPackets_PS txPackets_PS rxKB_PS txKB_PS" + }, + { + "index": "os", + "sourcetype": "df", + "source": "df", + "host": "$host$", + "_raw": "$nic$ $rx_p$ $tx_p$ $rx_kb$ $tx_kb$" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/nixOS/bandwidth.yml" + }, + { + "name": "cpu.yml", + "category": "nixOS", + "yaml_content": "global:\n samplesDir:\n - $GOGEN_HOME/examples/nixOS\ngenerators:\n - name: cpu\n fileName: $GOGEN_HOME/examples/nixOS/cpu.lua\n options:\n minCPU: 30.0\n maxCPU: 75.0\n numCPUs: 8\nsamples:\n - name: cpu\n description: Generate CPU Metrics\n notes: >\n Generates CPU usage in the form of Splunk's UNIX TA\n generator: cpu\n interval: 60\n tokens:\n - name: host\n type: fieldChoice\n srcField: host\n sample: allhosts.csv\n disabled: true\n lines:\n - index: os\n sourcetype: cpu\n source: cpu\n host: $host$\n _raw: |-\n CPU pctUser pctNice pctSystem pctIowait pctIdle\n all $pctUserAll$ 0 $pctSystemAll$ $pctIowaitAll$ $pctIdleAll$\n - index: os\n sourcetype: cpu\n source: cpu\n host: $host$\n _raw: $CPU$ $pctUser$ 0 $pctSystem$ $pctIowait$ $pctIdle$", + "parsed_content": { + "global": { + "samplesDir": [ + "$GOGEN_HOME/examples/nixOS" + ] + }, + "generators": [ + { + "name": "cpu", + "fileName": "$GOGEN_HOME/examples/nixOS/cpu.lua", + "options": { + "minCPU": 30.0, + "maxCPU": 75.0, + "numCPUs": 8 + } + } + ], + "samples": [ + { + "name": "cpu", + "description": "Generate CPU Metrics", + "notes": "Generates CPU usage in the form of Splunk's UNIX TA\n", + "generator": "cpu", + "interval": 60, + "tokens": [ + { + "name": "host", + "type": "fieldChoice", + "srcField": "host", + "sample": "allhosts.csv", + "disabled": true + } + ], + "lines": [ + { + "index": "os", + "sourcetype": "cpu", + "source": "cpu", + "host": "$host$", + "_raw": "CPU pctUser pctNice pctSystem pctIowait pctIdle\nall $pctUserAll$ 0 $pctSystemAll$ $pctIowaitAll$ $pctIdleAll$" + }, + { + "index": "os", + "sourcetype": "cpu", + "source": "cpu", + "host": "$host$", + "_raw": "$CPU$ $pctUser$ 0 $pctSystem$ $pctIowait$ $pctIdle$" + } + ] + } + ] + }, + "comments": {}, + "file_path": "examples/nixOS/cpu.yml" + } + ], + "documentation": { + "reference": "Gogen Reference\n\n\nThis document will walk through all the objects in the configuration and Lua API and attempt to document them fully.\n\nEnvironment Variables\n\n\n| Environment Variable | Expected Value | Description |\n|------------------------|------------------|------------------------------------------------------------------------------|\n| GOGEN\\_EXPORT | 1 or unset | Specifies whether Gogen is running to export configs. If set to export, internal defaults aren't added and a few more miscellaneous things.\n| GOGEN\\_INFO | 1 or unset | Specifies whether to turn on info level logging\n| GOGEN\\_DEBUG | 1 or unset | Specifies whether to turn on debug level logging\n| GOGEN\\_GENERATORS | integer | Number of generator threads for Gogen to run\n| GOGEN\\_OUTPUTTERS | integer | Number of output threads for Gogen to run\n| GOGEN\\_OUTPUTTEMPLATE | name of template | Name of an output template to use.\n| GOGEN\\_OUT | outputter | Name of outputter to use, stdout, file, http, etc\n| GOGEN\\_FILENAME | filename | When outputter is file, output to this filename\n| GOGEN\\_URL | URL | When outputter is http, URL to send data to\n| GOGEN\\_HEC_TOKEN | Token | When outputter is HTTP, use the token for authentication to Splunk's HTTP Event Collector\n| GOGEN\\_SAMPLES\\_DIR | directory | Directory to find sample configs\n| GOGEN\\_CONFIG | file | Path to a full configs\n| GOGEN\\_CONFIG\\_DIR | directory | Directory to use as a base config directory, contains sample, templates, generator directories.\n\nConfiguration\n\n\nThe configuration \n\n| Section | Description |\n|------------|--------------------------------------------------------------------------------------------------------------------|\n| global | Defines global parameters, such as output |\n| generators | Defines custom generators, written in Lua, which can greatly extend gogen's capabilities |\n| raters | Defines raters, which allow you to rate the count of events or value of tokens based on time or custom Lua scripts |\n| samples | Define sample configurations, which is the core data structure in Gogen |\n| mix | Defines mix configurations, which allow you to reuse existing sample configurations in new configurations |\n| templates | Defines output templates, which allow you to format the output of Gogen using Go's templating language |\n\nGlobal\n\n\nGlobal options:\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| debug | Turns on debug internal logging output | bool |\n| verbose | Turns on verbose internal logging output | bool |\n| generatorWorkers | Sets the number of threads generating output tasks | int |\n| outputWorkers | Sets the number of threads outputting output tasks | int |\n| rotInterval | Interval in seconds to output internal statistics to stderr | int |\n| output | Set the output plugin to use | string |\n| samplesDir | Sets the directory to look for Sample YAML, CSV or .Samples files | string list |\n| cacheIntervals | Sets the number of intervals to reuse generated events | int |\n\n\nOutput\n\n\nOutput options:\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| fileName | For file output, sets the file name to output to | string |\n| maxBytes | For file output, sets the max bytes before rolling a new file | int64 |\n| backupFiles | For file output, sets the number of files to keep before discarding older files | int |\n| bufferBytes | For HTTP, S2S and other outputs, sets the number of bytes to buffer before flushing | int |\n| outputter | Sets the output module to use, currently supports devnull, file, http and stdout | string |\n| outputTemplate | Set the output template to format output, builtins include csv, json, splunkhec | string |\n| endpoints | For http, or potentially others, lists endpoints to send data to. | string list |\n| headers | For http, sets headers | string obj |\n| protocol | For network, set to `tcp` or `udp` | string |\n| timeout | For network based outputs, a time in seconds, default `10s` | string |\n\nSample\n\n\nSamples are represented as a list of sample objects, which can consist of the following configuration options:\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Name of the sample | string |\n| description | Description of the sample | string |\n| notes | Notes about the sample | string |\n| disabled | Sets whether the sample should be disabled and not generate events | bool |\n| generator | Sets the sample to use a custom generator as defined in the generators stanza | string |\n| rater | Sets the sample to use a custom rater as defined in the raters stanza | string |\n| interval | Sets the interval, in seconds, between generations of this sample | int |\n| delay | Sets the delay, in seconds, before starting generation for the first time | int |\n| count | Sets the number of events to generate each sample | int |\n| earliest | Sets the beginning of the time window to generate an event in for this interval (ex: -1m) | string |\n| latest | Sets the end of the time window to generate an event in for this interval (ex: now) | string |\n| begin | Sets the timestamp to begin generation at (ex: -1h) | string |\n| end | Sets the timestamp to end generation at (ex: now). If unspecified, generates in real time | string |\n| endIntervals | Sets generation to run for `endInterval` intervals | int |\n| randomizeCount | Percentage of randomness of `count` events. Ex: 0.2 will randomly increase count +/- 20% | float |\n| randomizeEvents | Randomize the events from the sample when picking events. By default will pick top X events | bool |\n| tokens | List of tokens (see below) | token |\n| lines | List of line objects. Arbitrary key/value pairs to be used for generation. | list string obj\n| field | Sets the default field to replace in (default '_raw') | string |\n| fromSample | Bring in lines from another named sample | string |\n| singlePass | Allows disabling SinglePass optimization, if for example you have chained replacements | bool |\n\nToken\n\n\nTokens are the core unit of the replacement engine, and they contain the following configuration options:\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Name of the token | string |\n| format | Format of the replacement, either `regex` or `template`. Default `template` | string |\n| token | Replacement text to find. Required for `regex`, for `template` defaults to `$name$` | string |\n| type | Sets the type of replacement. See token types below. | string |\n| replacement | Value to use for the replacement. Depends on the token type (see below) | string |\n| group | Token group. All items from the same group will pick the same index across multiple tokens | int |\n| sample | For choice types, pulls the items from another sample | string |\n| field | Field to replace into, defaults to `_raw` | string |\n| srcField | Field to replace from, used in `fieldChoice` | string |\n| precision | For `float` `random` or `rated` tokens, how many decision points to generate | int |\n| lower | Lower value for a `random` or `rated` token | int |\n| upper | Upper value for a `random` or `rated` token | int |\n| length | Length of a `random` `string` or `hex` replacement | int |\n| weightedChoice | Used for `weightedChoice` type, a list of objects containing `choice` and `weight` (`int`) | list of obj |\n| fieldChoice | Used for `fieldChoice` type, a list of objects containing fields and values | list string obj |\n| choice | Used for `choice` type, a list of strings to use for replacements | list of string |\n| script | LUA script to use for replacement. | string |\n| init | Initialize keys and values in the Lua engine | object |\n| rater | Use the specified rater to rate this token (see below) | string |\n| disabled | Disables this sample. | bool |\n\nToken types:\n\n| Type | Description |\n|------------------|------------------------------------------------------------------------------------------------|\n| timestamp | Timestamp in strftime format |\n| gotimestamp | Timestamp in [go timestamp format](https://golang.org/pkg/time/#pkg-constants). This is signifcantly more performant than strftime. |\n| epochtimestamp | Timestamp in seconds since the epoch. | \n| static | Replaces with a static string |\n| random | Replaces the token with random values. Valid replacement values: `int`, `float`, `string`, `hex`, `guid`, `ipv4`, or `ipv6` |\n| rated | Replaces the token with a rated value. Valid replacement values: `int`, `float` |\n| choice | Replaces from the `choice` stanza, which is a list |\n| weightedChoice | Replaces from the `weightedChoice` stanza, which is a list of objects containing weight (`int`) and choice |\n| fieldChoice | Replaces from the `fieldChoice` stanza, which is an object containing values. Selects field based on `field` stanza. |\n| script | Replaces using a lua script, which is defined inline. |\n\nMix\n\n\nMixes allow grabbing other full configurations, overriding a few parameters, and creating a new config.\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| sample | Name of the sample. Path to a config file or a public config (ex: coccyx/weblog) | string |\n| interval | Overrides `interval` of sample. | int |\n| count | Overrides `count` of sample. | int |\n| begin | Overrides `begin` of sample. | string |\n| end | Overrides `end` of sample. | string |\n| endIntervals | Overrides `endIntervals` of sample. | int |\n| realtime | Sets sample to realtime. This exists because if end is set, this will override to realtime. | bool |\n\nRaters\n\n\nRaters will dynamically determine value based on the time of day or a custom script.\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Name of the rater | string |\n| type | Type of the rater. Either `config` or `script` | string |\n| script | For `script` rater, specifies a Lua script to use to rate. | string |\n| options | Options to pass to the config rater. See [here](https://github.com/coccyx/gogen/blob/master/tests/rater/fullraterconfig.yml) for example. | object |\n| init | Initialize Lua variables for `script` rater. | object |\n\nTemplate\n\n\nTemplates let the user change how data is output using Go's template language. See [here](https://github.com/coccyx/gogen/blob/master/examples/tutorial/tutorial4.yml) for an example.\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Name of the template | string |\n| header | Header for the template | string |\n| row | Row for the template | string |\n| footer | Footer for the template | string |\n\nGenerators\n\n\nGenerators let the user define custom logic in Lua for how events should be generated. Generators contain a configuration component as well as an API the user can access to send events and access configuration data.\n\n\n| Setting | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Name of the generator | string |\n| init | Object of key value pairs to initialize in the `state` variable in in the Lua engine | object |\n| options | Object of key value pairs to be placed in the `options` variable in the Lua engine. Unlike `init`, can pass complex structures. | object |\n| script | Script to execute. | string |\n| fileName | File on disk containing the Lua script. | string |\n| singleThreaded | Execute SingleThreaded or not. Scripts may be wary of stomping on state in multithreaded mode. | bool |\n\nGenerator API\n\n\nThe Lua environment provides a rich set of APIs to access running state inside of Gogen. This documents the global variables as well as all the functions and their parameters.\n\nGlobals\n\n\n| Variable | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| state | State from the `init` setting in config | table |\n| options | Options passed to the generator from config, as userdata | userdata |\n| lines | Lines configured in config, as a table of tables | table |\n| count | Count to generate for this interval | number |\n| earliest | Earliest time to generate for, in epoch time | number |\n| latest | Latest time to generate for, in epoch time | number |\n| now | Current time, in epoch time | number |\n\n\nsleep\n\n\nsleep uses Go's time.sleep to sleep the generator thread.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| duration | Duration to sleep in seconds | int64 |\n\ndebug\n\n\ndebug outputs debug output to stderr.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| message | Message to output | string |\n\nInfo\n\n\ninfo outputs info output to stderr.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| message | Message to output | string |\n\nreplaceTokens\n\n\nreplaceTokens calls Gogen's replacement engine, and replaces all tokens in a particular line.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| event | Takes a Lua table of key/value pairs to replace tokens in. Generally retrieved from `getLine` | table |\n| choices | Dictionary of choices, passed back in as a return from a previous `replaceTokens` call | userdata |\n| replaceFirst | Replace tokens set via `setTokens` first if `true` | bool |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| event | Event with all tokens replaced | table |\n| choices | Dictionary of choices | userdata |\n\nsend\n\n\nsend sends events to the outputter.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| events | Takes a Lua table of tables of key/value pairs to output | table |\n\nsendEvent\n\n\nsendEvent sends a single event to the outputter.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| events | Takes a Lua table key/value pairs to output | table |\n\nround\n\n\nround rounds a Lua Number to the specified precision.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| number | Number to round | number |\n| precision | Signifcant digits to round to | number |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| number | Rounded number | number |\n\nsetToken\n\n\nsetToken sets a token for Gogen's replacement engine.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Token name, represented by `$name$` in the event | string |\n| value | Token value | string |\n| field | Field to replace in, defaults to `_raw` | string |\n\nremoveToken\n\n\nremoveToken removes a token set via `setToken`.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| name | Token name to remove | string |\n\ngetLine\n\n\ngetLine returns a line from the sample config.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| line | Line number to return | number |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| line | Table of key/value parameters from the sample config | table |\n\ngetLines\n\n\ngetLines returns all lines from the sample config.\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| lines | Table of tables of key/value parameters from the sample config | table |\n\ngetChoice\n\n\ngetChoice returns all choices from the sample config for a given token.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| token | Token to retrieve | string |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choices | Table of choices of key/value parameters from the sample config | table |\n\ngetChoiceItem\n\n\ngetChoice returns a choice item from the sample config.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| token | Token to retrieve | string |\n| index | Retrieve choice at line `index` | number |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choice | Choice at `index` from the sample config | string |\n\ngetFieldChoice\n\n\ngetFieldChoice returns all fieldChoices from the sample config for a given token.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| token | Token to retrieve | string |\n| field | Field to retrieve | string |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choices | Table of choices of key/value parameters from the sample config | table |\n\ngetFieldChoiceItem\n\n\ngetFieldChoiceItem returns a choice from the sample config.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| token | Token to retrieve | string |\n| field | Field to retrieve | string |\n| index | Retrieve choice at line `index` | number |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choice | Choice of `field` at `index` from the sample config | string |\n\ngetWeightedChoiceItem\n\n\ngetWeightedChoiceItem returns a choice from the sample config.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| token | Token to retrieve | string |\n| index | Retrieve choice at line `index` | number |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choice | Choice `index` from the sample config | string |\n\ngetGroupIdx\n\n\ngetGroupIdx returns the index for a particular choice.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choices | Choices, returned from a previous `replaceTokens` call | userdata |\n| group | Index of group to return | number |\n\n*Returns:*\n\n| Return | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| choice | Index of the row chosen for a particular group | numbe |\n\n\nsetTime\n\n\nsetTime sets the time for a particular interval.\n\n| Parameter | Description | Type |\n|------------------|------------------------------------------------------------------------------------------------|-------------|\n| time | Epoch Time to set time to | number |", + "tutorial": "Configuring Gogen\n\n\nGogen is the spiritual successor to my original [Eventgen](https://github.com/splunk/eventgen), and as such it shares many configuration concepts in common. However, Gogen as the successor has been designed to work around a number of deficiencies in Eventgen's original configuration format.\n\nGogen was designed to be configured from a single file. This makes moving configurations around very simple. Managing the data Gogen references is painful from a single file, so it also allows for referencing other files. For example, choice token types allow choosing items from fields in other samples, and these samples can be referenced to files on the file system. When publishing the configurations, Gogen will take it's in-memory representation which has joined all the configuration data together, and generate a single file version of this configuration. Later, if desired, Gogen can deconstruct this single file representation back into component files to make editing easier.\n\nConfig File Overview\n\n\nGogen is configured via a YAML or JSON based configuration. Lets look at a very simple example configuration:\n\n samples:\n - name: tutorial1\n interval: 1\n endIntervals: 5\n count: 1\n randomizeEvents: true\n\n tokens:\n - name: ts\n format: template\n type: timestamp\n replacement: \"%b/%d/%y %H:%M:%S\"\n\n lines:\n - _raw: $ts$ line1\n - _raw: $ts$ line2\n - _raw: $ts$ line3\n\nThis example is in YAML. Gogen configurations are made up of Samples, which contain some configuration, tokens, and lines. In this example, we will generate 1 event (`count: 1`) from a random line (`randomizeEvents: true`) every 1 second (`interval: 1`) for a total of 5 intervals (`endIntervals 5`). When `endIntervals` is set, we will go back that number of intervals and just work as fast as we can to generate that number of events. Gogen can also keep generating and generate in realtime, which we'll cover a bit later.\n\nYou can see we define here a top level item called samples, which is a list of objects. There are a few top level directives which control Gogen:\n\n| Section | Description |\n|------------|--------------------------------------------------------------------------------------------------------------------|\n| global | Defines global parameters, such as output |\n| generators | Defines custom generators, written in Lua, which can greatly extend gogen's capabilities |\n| raters | Defines raters, which allow you to rate the count of events or value of tokens based on time or custom Lua scripts |\n| samples | Define sample configurations, which is the core data structure in Gogen |\n| mix | Defines mix configurations, which allow you to reuse existing sample configurations in new configurations |\n| templates | Defines output templates, which allow you to format the output of Gogen using Go's templating language |\n\nWe will cover these in future examples.\n\nTo run this example, from the Gogen repo directory:\n\n gogen -c examples/tutorial/tutorial2.yml gen\n\nRaters & Scripts\n\n\nTwo of the most important concepts in Gogen are the concepts of rater and scripts. Raters allow the user to modify the count of events or the value of tokens based on the time. Scripts allow the user the extend Gogen's logic through simple [Lua](http://lua.org/) scripts. Lets see this in action.\n\n raters:\n - name: eventrater\n type: config\n options:\n MinuteOfHour:\n 0: 1.0\n 1: 0.5\n 2: 2.0\n - name: valrater\n type: script\n script: >\n return options[\"multiplier\"]\n options:\n multiplier: 2\n\n samples:\n - name: tutorial2\n begin: 2012-02-09T08:00:00Z\n end: 2012-02-09T08:03:00Z\n interval: 60\n count: 2\n rater: eventrater\n\n tokens:\n - name: ts\n format: template\n type: timestamp\n replacement: \"%b/%d/%y %H:%M:%S\"\n - name: linenum\n format: template\n type: script\n init:\n id: \"0\"\n script: >\n state[\"id\"] = state[\"id\"] + 1\n return state[\"id\"]\n - name: val\n format: template\n type: random\n replacement: int\n lower: 1\n upper: 5\n - name: rated\n format: template\n type: rated\n replacement: int\n lower: 1\n upper: 3\n rater: valrater\n\n lines:\n - _raw: $ts$ line=$linenum$ value=$val$ rated=$rated$\n\nGo ahead and run this.\n\n gogen -c examples/tutorial/tutorial2.yml\n\nLet's examine this config in detail, because it introduces a number of important concepts. First, this config, rather than running over a specified number of intervals, is setup to run over a specific time period by setting the `begin` and `end` clauses of the sample. It is set to generate `count: 2` events, once per minute, with an `interval` of `60`. Lastly, it sets a rater of `eventrater`, which we will explain in detail.\n\nThere are two types of raters, `config` and `script`. The last, `default`, always returns 1. Raters return a value to _multiply_ events by. If an event has a count of 2, it'll be multiplied by the value returned by the `eventrater` rater. The `config` rater will rate events by the time of the event. In this case, the event will always be between 8 AM and 8:03 AM, so we've only set a few `MinuteOfHour` options, but canonically [it will look more like this](../tests/rater/configrater.yml). The `config` rater has three options, `MinuteOfHour`, `HourOfDay` and `DayOfWeek`. It will look up the current time of the event in each of these three options, and if found, multiply the `count` (set by `count` in the event) by this floating point value, and once all the multiplications are done, round to the nearest whole number.\n\nIn our example, we should see 2 events generated in the first minute, 1 event in the second minute, and 4 events in the last minute.\n\nNow, lets look at the tokens for this sample. The second token introduces a custom script token. This script is written in [Lua](http://lua.org/), which is very [easy to learn](https://www.lua.org/pil/1.html). Most people who are versed in scripting or programming can pick up Lua merely by modifying the examples provided with Gogen. The `linenum` token is very simple, as all it does is return a monotonically increasing identifier to be used as our line number. Useful, but simple. It's initialized with the `init` configuration directive, to create an entry in the `state` table in Lua under the `id` item, and set it to zero. The script increments this id and returns the value on every iteration.\n\nThe next two tokens show the difference between a random token and a rated token. Both are configured nearly identically. `val` and `rated` both have a `replacement` of int, a `lower` and `upper` configuration directive dictating the value ranges to be generated. The difference is `rated` is run through a rater first, the same way event counts are, by multiplying the random value from the token times the value returned by the rater. In this case, the rater is a simple script, which looks up a value configured in `options` in the YAML and returns that value as the multiplier.\n\nThis example is much more complicated than the original, but begins to show off the true power of what Gogen can do with easy to understand and share configurations.\n\nTokens & the power of sample files\n\n\nGogen was enhanced to include the easy addition of custom scripts to enhance the base functionality, but the core ability of Gogen is still its ability to generate real-looking data by simply subtituting data randomly from lists of options. Eventgen had this capability, and Gogen has *significantly* enhanced it by making it much easier to manage and configure.\n\nTokens have two different `format` options, `regex` and `template`. `regex` will search a line for a regex pattern and replace any matches with the value from the token. `template` is *far* preferred and *significantly more performant*, and it will replace, by default, a token matching `$$` in the original event. Template is intended if you're manually crafting an event, no need to waste the CPU cycles to do regex matches when a simple string match will do. If you have a token named `foo`, it will search for the text `$foo$` in the original event and replace that.\n\nTokens also have a number of `type` settings. See the [Reference manual](Reference.md) for a full list, but the primary categories are `timestamp`, `static`, `random`, `rated`, `choice` (with `fieldChoice` and `weightedChoice`) and `script`. Most important are the `choice` settings, and thusly the section about sample files. Choice settings can be determined by bringing in data from external files. Lets examine tutorial 3 in more detail for some examples:\n\n name: tutorial3\n begin: 2012-02-09T08:00:00Z\n end: 2012-02-09T08:03:00Z\n interval: 60\n count: 2\n rater: eventrater\n tokens:\n - name: ts\n format: regex\n token: (\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2},\\d{3})\n type: gotimestamp\n replacement: \"2006-01-02 15:04:05.000\"\n - name: host\n format: template\n type: choice\n field: host\n choice:\n - server1.gogen.io\n - server2.gogen.io\n - name: transtype\n format: regex\n token: transType=(\\w+)\n type: weightedChoice\n weightedChoice:\n - weight: 3\n choice: New\n - weight: 5\n choice: Change\n - weight: 1\n choice: Delete\n - name: integerid\n format: template\n type: script\n init:\n id: \"0\"\n script: >\n state[\"id\"] = state[\"id\"] + 1\n return state[\"id\"]\n - name: guid\n format: template\n type: random\n replacement: guid\n - name: username\n format: template\n type: choice\n sample: usernames.sample\n - name: markets-city\n format: template\n token: $city$\n type: fieldChoice\n sample: markets.csv\n srcField: city\n group: 1\n - name: markets-state\n format: template\n token: $state$\n type: fieldChoice\n sample: markets.csv\n srcField: state\n group: 1\n - name: markets-zip\n format: template\n token: $zip$\n type: fieldChoice\n sample: markets.csv\n srcField: zip\n group: 1\n - name: value\n format: regex\n token: value=(\\d+)\n type: random\n replacement: float\n precision: 3\n lower: 0\n upper: 10\n\n lines:\n - index: main\n host: $host$\n sourcetype: translog\n source: /var/log/translog\n _raw: 2012-09-14 16:30:20,072 transType=ReplaceMe transID=$integerid$ transGUID=$guid$ userName=$username$ city=\"$city$\" state=$state$ zip=$zip$ value=0\n\nTo run this tutorial run:\n\n gogen -cd examples/tutorial/tutorial3 -ot json gen\n\nThe first thing to note about this config: no samples item. All the items from the sample are top level directives. This is because we're running this config as a [config directory](examples/tutorial/tutorial3) instead of a [full config](examples/tutorial/tutorial2.yml), thusly running `gogen -cd` instead of `gogen -c`. Config directories allow for breaking out out of the items to be managed into individual files instead of being combined into one. This allows, for example, to share raters amongst samples without copying and pasting them amongst the full config files. In this case, we have subdirectories inside the config directory called `samples` and `raters` which contain the same items which would be list items of a full config seperated into their own files. Gogen will walk that directory and load anything with an extension of `yml`, `json`, `csv`, or `sample`. When we want to share this config, Gogen allows for export functions which will combine all these files into a single file config for sharing.\n\nThe next thing to note about this config is that we've added support for multiple fields in the sample's lines. This is why we suggested the `-ot json` directive in the command line. This sets the `outputTemplate` to `json`, which can also be specified in the `global` directive. `json` will show you easily there is multiple fields of metadata flowing through the gogen event, but by default only `_raw` is output, matching with how Splunk treats data.\n\nNow, let's look through the tokens to better understand a more complicated token configuration. Firstly, if we look at the timestamp token, it is using a `regex` match, which we normally don't do in this type of example except to show how it's done. Note with the regex, we need to ensure we have a capture group in parenthesis to tell Gogen where to replace. This timestamp uses [Go's time format syntax](https://gobyexample.com/time-formatting-parsing), which uses a reference time of `Mon Jan 2 15:04:05 MST 2006`. When you want a timestamp to look like `%Y-%m-%d %H:%M:%S` in strftime format, you would give gotimestamp `2006-01-02 15:04:05`. It takes a bit of getting used to, but it's significantly more performant than strftime, so it's recommended to use this format.\n\nNext is the `host` token. This introduces two new options, the first is a type of `choice`. `choice` tokens choose from a list of items to get the value from which to substitute. In this case, we've articulated the values right in the configuration, but `choice`, `fieldChoice` and `weightedChoice` tokens can also get their values from external sample files in flat text file or csv format, which you can see in the `username` token further down. Lastly, it's important to note the `field` directive, which substitutes `host` into the `host` field of the sample line. Field can be declared on any token to tell it to replace items in any field in the event, with the default being to replace in `_raw`.\n\nThe next token we want to look at is `transtype`. `transtype` is of type `weightedChoice`, which chooses tokens based on a weighting algorithm. A token of weight `5` will be 5 times more likely to be chosen than a token of weight `1`. This is very helpful when trying to model data that looks more like real world data, which isn't necessarily going to have an even distribution.\n\nNext, lets take a look at a series of tokens, the ones prefixed with markets. These tokens introduce a new type, `fieldChoice`, and a few new directives, `srcField` and `group`. `fieldChoice` tokens will select a replacement from a field from a tablular dataset, like a CSV or a list of YAML objects. What makes these even more useful, is that the choice we make can be carried across multiple token substitutions. This is what the `group` clause is for, and it can be used in any of the choice token types. Any time a `group` clause is present, the same choice will be used across any matched group number.\n\nGenerators & Templates\n\n\nAs I was rewriting the original Eventgen concept that was to become Gogen, I really wanted to preserve Eventgen's ability to be expandable. Eventgen allows users to ship custom generators in their Eventgens, so I wanted to replicate that in Gogen. However, unlike python which is a dynamic, interpreted language, Gogen is written in Go which is a strongly typed, compiled & statically linked language. In order to give that capability, we do that through custom Lua generators, which have a rich API to allow the Lua scripts to interact with Gogen. This allows for higher performance, as most of the work is handled in Go code, and it allows for the script developer to minimize the amount of things they need to replicate in their own code.\n\nSecondly, I wanted the ability to customize how we output data. Making that format expandable will future proof Gogen and allow field expandability to model new types of data formats. Gogen can output raw text, JSON, CSV and other formats by default, but custom templates allow for more complicated data structures.\n\nFor the next example, we're going to show both of these features generating an ini-style configuration file just for giggles.\n\n global:\n output:\n outputTemplate: inifile\n outputter: stdout\n\n generators:\n - name: indexes.conf\n script: |\n header = \"# Generated at $ts$\"\n\n events = { }\n for i=0,2 do\n line = getLine(i)\n line[\"header\"] = header\n line = replaceTokens(line)\n\n if line[\"maxHotSpanSecs\"] ~= nil then\n frozenTimePeriodInSecs = tonumber(line[\"maxHotSpanSecs\"]) * 6\n line[\"frozenTimePeriodInSecs\"] = tostring(frozenTimePeriodInSecs)\n end\n\n line[\"homePath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/db\"\n line[\"coldPath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/colddb\"\n line[\"thawedPath\"] = \"$SPLUNK_DB/\"..line[\"index\"]..\"db/thaweddb\"\n table.insert(events, line)\n end\n send(events)\n\n samples:\n - name: inifile\n\n generator: indexes.conf\n\n count: 1\n endIntervals: 1\n\n tokens:\n - name: ts\n field: header\n format: template\n type: timestamp\n replacement: \"%b/%d/%y %H:%M:%S\"\n\n lines:\n - index: history\n maxDataSize: 10\n - index: summary\n - index: _internal\n maxDataSize: 1000\n maxHotSpanSecs: 432000\n\n templates:\n - name: inifile\n header: '{{ .header }}'\n row: |\n [{{ .index }}]\n homePath = {{ .homePath }}\n coldPath = {{ .coldPath }}\n thawedPath = {{ .thawedPath }}\n {{ if .maxDataSize -}}\n maxDataSize = {{ .maxDataSize }}\n {{ end }}{{ if .maxHotSpanSecs -}}\n maxHotSpanSecs = {{ .maxHotSpanSecs }}\n {{ end }}{{ if .frozenTimePeriodInSecs -}}\n frozenTimePeriodInSecs = {{ .frozenTimePeriodInSecs }}{{ end }}\n\nLet's go ahead and run this to get a look at the output:\n\n gogen -c examples/tutorial/tutorial4.yml\n\nAs you can see, it does indeed look like an ini file configuration. It's modeled after Splunk's indexes.conf. Once again, we're introducing several important new concepts. The first, is our `global` section, which we described earlier but it's the first time we've seen it. Here, global sets the outputTemplate which we're defining below.\n\nNext, let's look at the `generator` stanza. This aligns also with the `generator` directive in the sample stanza. We define here one generator, named indexes.conf, and we're using the `script` directive to define the script inline in the yaml, but we optionally could have pointed to a file with the `fileName` directive. This defines a relatively simple lua script, which we'll walk through in more detail.\n\nWe define a string named `header` which has a template variable in it. This value will be present in every row, primarily to handle a data model where all the data sent to the template engine needs to be in a map[string]string. In the future, we might enhance templates with a concept of global variables. We then declare a for loop to loop 3 times. We call `getLine`, which returns back a table (map[string]string equivalent) of one of our lines defined in the sample. Lines are numbered starting from 0. We call `replaceTokens` which calls back into Gogen's token replacement engine and substitutes any tokens we find in that line. Next we define a few more new entries in the line dynamically with some string concatenation and then insert it into our events table. Lastly, after the loop, we call `send` which sends our complete table to the outputter.\n\nIf you want to see what just the data looks like, without the template, it's easy to run:\n\n gogen -c examples/tutorial/tutorial4.yml -ot csv\n\nNow, let's look at the templates section. We define here a new template, called `inifile`, which uses [Go's template library](https://golang.org/pkg/text/template/). It should be pretty obvious to see what we're doing, if you've used any templating system in the past. A given line is passed to the templating engine, and each item in the map is available as a variable in the template, as you can see from the example.\n\nReplay\n\n\nSometimes you just want to walk through a set of data as it was originally created. Maybe you've found a log file that you just want to replay like it's happening right now, or you have some data in another system that is already structured and you want to be able to walk through it. In this example, I've exported a search from Splunk's \\_internal logs and I'm going to replay them like it's right now.\n\n samples:\n - name: tutorial5\n generator: replay\n\n tokens:\n - name: ts1\n type: timestamp\n format: regex\n token: (\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2})\n replacement: '%d/%b/%Y:%H:%M:%S'\n fromSample: results.csv\n\nLet's go ahead and run this:\n\n gogen -c examples/tutorial/tutorial5.yml\n\nNote, it will continue to just run. Hit `^C` to exit Gogen. This example pulls in the lines from a file called results.csv. It uses the `replay` generator, and it will walk through the file looking for all the timestamps it can find matching the regex token and it will attempt to parse them using the `replacement` format. It will look through all the lines and determine how long it should wait between each event based on the timings contained in the original file.\n\nMixes\n\n\nMuch of what users of Gogen need to do is to assemble a realistic set of data to test their use case. This is why we built the [config sharing system](Sharing.md). What if someone has already published something and you want to combine it with your own or another configuration? This is what we created mixes for.\n\n mix:\n - sample: $GOGEN_HOME/examples/tutorial/tutorial1.yml\n begin: now\n realtime: true\n count: 1\n interval: 1\n - sample: $GOGEN_HOME/examples/tutorial/tutorial2.yml\n begin: now\n realtime: true\n count: 1\n interval: 1\n - sample: $GOGEN_HOME/examples/tutorial/tutorial5.yml\n begin: now\n realtime: true\n\nThis last of our tutorial files should be pretty simple to grok. We're referencing 3 of our other tutorial files and we're combining them into a mix. We're overriding a few attributes of the original samples, and setting them all to generate in real time since before several of them were fixed time windows. If you run this:\n\n gogen -c examples/tutorial/tutorial6.yml\n\nYou'll see they generate in real time. End generation with another `^C`.\n\nConfig Done!\n\n\nYou're now a functional expert, assuming you've read this far, in Gogen configuration. Make sure to check out [more examples](Examples.md) where we list a number of examples developed by [myself](http://github.com/coccyx) and the community.\n\nUsing Gogen\n\n\nGogen also has a number of features to learn for using it day to day. Gogen has a number of options which will change its behavior. Let's look at the weblog example for some different usage patterns.\n\n gogen -c examples/weblog/weblog.yml\n\nThis will output 10 events. Let's output 10 events but over 10 seconds, one event per second, instead of all with the same timestamp. Setting `--endIntervals` or `-ei` will automatically set the beginning time back the number of intervals specified. Setting `--count` or `-c` will set the count to the specified number per interval.\n\n gogen -c examples/weblog/weblog.yml gen -c 1 -ei 10\n\nWe might get tired of typing the same config directives over and over again. To save Gogen settings to the environment, run:\n\n $(gogen -c examples/weblog/weblog.yml env)\n\nMaybe we'd prefer to see all the metadata associated with those events as well (if you don't have jq installed, drop the part after the `|`). Setting `--outputTemplate` or `-ot` will change the template we use to output. Raw, CSV and JSON come with gogen, and custom templates can be built using Go's templating language.\n\n gogen -ot json gen -c 1 -ei 10 | jq .\n\nLet's generate data over a specified window. Setting `--interval` or `-i` will set how many seconds will elapse between samples being generated. Setting `--begin` or `-b` to `-10s` uses [Splunk's relative time syntax](https://docs.splunk.com/Documentation/Splunk/6.5.1/SearchReference/SearchTimeModifiers) and says to start generation 10 seconds before now. Setting `--end` or `-e` to `now` stops generation at the current time.\n\n gogen gen -c 1 -i 2 -b -10s -e now\n\nWe're going to use a different sample now, so we need to unset our environment variables from before.\n\n $(gogen unsetenv)\n\nWe may want to just continue generating events when we're done. Setting `--sample` or `-s` will restict generation to just one sample, and setting `--realtime` or `-r` will tell Gogen to keep generating events in real time.\n\n gogen -c examples/tutorial/tutorial6.yml gen -s tutorial5 -b -5s -r\n\nWe may want to use Gogen for performance testing. In order to do this, we're probably going to need more than one thread generating or outputting information. For this we set `--generators` or `-g` to a number greater than one, or `--outputters` or `--out` to greater than 1. To see what's going on in the background, we set `--verbose` or `-v`.\n\n gogen -c examples/weblog/weblog.yml -o devnull -v -g 4 gen -c 1000 -ei 3000\n\nWe may want Gogen to generate static data, where outputting to a file would make sense. To output a CSV file for example, use `--output` and `--outputTemplate` together.\n\n gogen -c examples/csv/csv.yml -o file -f test.csv\n\nGogen provides a configuration sharing service which allows for easy sharing of examples. Let's see if someone has a weblog available through the service.\n\n gogen search weblog\n\nLet's see some more information about this weblog generator.\n\n gogen info coccyx/weblog\n\nLet's run it to see what it looks like.\n\n gogen -c coccyx/weblog\n\nTurns out it's the same one we've been running all along. Let's see what the configuration looks like:\n\n gogen pull coccyx/weblog .\n more weblog.yml\n\nThat's pretty verbose, it'd be easier to edit if it looked more like how we recommended building them above. Pull allows us to deconstruct the uploaded configs, run it with `--deconstruct` or `-d`.\n\n gogen pull -d coccyx/weblog .\n\nYou can also publish your own configs. First, we need to login to Gogen.\n\n gogen login\n\nThis will open a local webserver and redirect your browser to http://localhost:46436/Login. Once you authorize Gogen to talk to your Github account, you can push new gogen configs to the config sharing service under your Github login.\n\n gogen -c push .\n\nWrapping up\n\n\nHope you've enjoyed our tutorial! You're now an expert!", + "examples": "Examples\n\n\nIn addition to our tutorial, we've put together a number of good example configs to get you started. \n\n| Example | Description |\n|------------------------------------------------------------|---------------------------------------------------------|\n| [Weblog](../examples/weblog/weblog.yml) | Quientissential log. Example used throughout the tutorial. \n| [CSV](../examples/csv/csv.yml) | Generates CSV Data. Showcases Gogen can be used for use cases aside from time series data. \n| [UNIX](https://github.com/coccyx/gogen/tree/master/examples/nixOS) | This example showcases custom generators. Generates data like running `df`, `ps`, etc on a UNIX box the way Splunk's UNIX TA collects data. Variables correlate and make heavy use of the LUA Generators feature. \n| [Users](https://github.com/coccyx/gogen/tree/master/examples/generator) | This example also showcases LUA Generators. Simulates a group of users going through a set of actions. Show cases complex interplay that's only possible by writing code.\n\nIf you have relevant examples, send a PR and we'll add them to this list!" + }, + "metadata": { + "total_examples": 15, + "documentation_sections": [ + "reference", + "tutorial", + "examples" + ], + "generated_at": "2025-03-01T14:45:34.038426" + } +} \ No newline at end of file diff --git a/llmcontext/requirements.txt b/llmcontext/requirements.txt new file mode 100644 index 0000000..5f1fd88 --- /dev/null +++ b/llmcontext/requirements.txt @@ -0,0 +1,2 @@ +pyyaml>=6.0.1 +markdown>=3.5.2 \ No newline at end of file diff --git a/llmcontext/setup_venv.sh b/llmcontext/setup_venv.sh new file mode 100755 index 0000000..dd1ee22 --- /dev/null +++ b/llmcontext/setup_venv.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Get the script's directory and the root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +VENV_DIR="$ROOT_DIR/.pyvenv" + +# Remove existing virtual environment if it exists +rm -rf "$VENV_DIR" + +# Create new virtual environment +python3 -m venv "$VENV_DIR" + +# Activate virtual environment +source "$VENV_DIR/bin/activate" + +# Upgrade pip +pip install --upgrade pip + +# Install requirements from llmcontext directory +pip install -r "$SCRIPT_DIR/requirements.txt" + +echo "Virtual environment setup complete in $VENV_DIR" \ No newline at end of file From b4dbcd09af31455e66885b22f995810976e52fa3 Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Sun, 2 Mar 2025 11:34:19 -0800 Subject: [PATCH 07/51] Last commit to up version to 0.11.0 --- README/TODO.md | 7 +++++++ RELEASE_NOTES.md | 33 +++++++++++++++++++++++++++++++++ VERSION | 2 +- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 RELEASE_NOTES.md diff --git a/README/TODO.md b/README/TODO.md index 5053f39..b9dbf92 100644 --- a/README/TODO.md +++ b/README/TODO.md @@ -29,3 +29,10 @@ * Add timemultiple * Consider finding a way to break up config package and refactor using better interface design * Unit test coverage 90% + +## New Config Generator and Sharing System +* Update config sharing system to login via a hosted website to Github to validate username +* Share configs via a private S3 bucket rather than GitHub gists +* Create chat based UI for generating configs from LLM +* Make sharing via the web as easy as clicking a button +* Remove old GitHub code from Gogen \ No newline at end of file diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..eb72f77 --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,33 @@ +# Gogen Release Notes + +## Version 0.11.0 + +### Breaking Changes +- Removed Splunk modinput and Splunk app support + - All Splunk-specific functionality has been removed to focus on core event generation capabilities + - Users relying on Splunk integration will need to use alternative methods for data ingestion + +### Improvements +- Significantly improved test coverage across multiple components: + - Added network testing suite + - Added devnull and stdout output tests + - Enhanced HTTP output testing + - Added RunOnce operation tests + - Improved timer and core functionality tests + +### Infrastructure +- Migrated from Travis CI to GitHub Actions +- Added Coveralls integration for code coverage reporting +- Updated deployment process + +### Bug Fixes +- Fixed issue with long intervals not shutting down in a reasonable time +- Fixed timezone extraction bug in tests +- Addressed flaky test behaviors + +### Internal Changes +- Refactored core components for better maintainability +- Updated configuration handling +- Enhanced logging system reliability + +For more information, please refer to the [documentation](README/Reference.md). \ No newline at end of file diff --git a/VERSION b/VERSION index 2d993c4..d9df1bb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.7 +0.11.0 From f53c124fc2831f4afc6803bc3a2fa85bbe2ad22a Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Sun, 16 Mar 2025 11:08:32 -0700 Subject: [PATCH 08/51] Updating API (#52) * Updating deps, adding test coverage, removing some things, updating version * Tests/increase coverage (#45) * Adding some more test coverage. Lots more to be done, but good enough for now * adding github actions * fixing flaky test * ignoring unix timestamp, other coverage is fine * fixed the wrong test * adding network tests * adding test for RunOnce * adding devnull and stdout tests * Adding coveralls to CI * fixing Elastic HTTP test * fixing timezone extraction bug in the test * changing goveralls invocation method * Adding deploy steps * Removed modinput and Splunk app support (#47) * Removed modinput and Splunk app support (#48) * Adding fix for long intervals not shutting down in a reasonable time (#49) * adding LLM context generator script (#50) * Last commit to up version to 0.11.0 * fixing CI for master branch * fixing env vars for docker * Refactoring API to use Python 3. Added support for a local dev environment. * Fixing database export bug. Adding support for GOGEN_APIURL to use local API * Adding local dev environment for gogen-api. Updated logging. Added pulling GitHub gist in the API rather than the client. * adding tests for Gogen API * Refactored API to store configurations in S3. * Local API now primed from local configs rather than backup. * Now can deploy production API. Various fixes for production. --- .cursor/rules/gogen-api.mdc | 16 ++ .github/workflows/ci.yml | 45 +++- .gitignore | 5 + README.md | 2 +- gogen-api/Dockerfile | 13 ++ gogen-api/README.md | 310 ++++++++++++++++++++++++++ gogen-api/api/db_utils.py | 39 ++++ gogen-api/api/get.py | 188 ++++++++++++++++ gogen-api/api/list.py | 55 +++++ gogen-api/api/logger.py | 32 +++ gogen-api/api/s3_utils.py | 160 ++++++++++++++ gogen-api/api/search.py | 70 ++++++ gogen-api/api/upsert.py | 129 +++++++++++ gogen-api/backup_restore.py | 84 +++++++ gogen-api/create_local_table.py | 50 +++++ gogen-api/deploy_lambdas.sh | 143 ++++++++++++ gogen-api/docker-compose.yml | 48 ++++ gogen-api/get.py | 47 ---- gogen-api/get_schema.py | 35 +++ gogen-api/list.py | 25 --- gogen-api/requirements.txt | 2 + gogen-api/search.py | 29 --- gogen-api/setup_local_db.sh | 26 +++ gogen-api/start_dev.sh | 69 ++++++ gogen-api/stop_dev.sh | 8 + gogen-api/table_schema.json | 19 ++ gogen-api/template.yaml | 103 +++++++++ gogen-api/test_dynamodb.py | 59 +++++ gogen-api/test_s3.py | 65 ++++++ gogen-api/upsert.py | 44 ---- internal/github.go | 75 ------- internal/gogen.go | 40 +++- internal/gogen_test.go | 380 +++++++++++++++++++++++++++++--- internal/share.go | 345 ++++++++++++++--------------- internal/share_test.go | 179 ++++++++++++--- main.go | 12 +- setup_venv.sh | 81 +++++++ 37 files changed, 2551 insertions(+), 481 deletions(-) create mode 100644 .cursor/rules/gogen-api.mdc create mode 100644 gogen-api/Dockerfile create mode 100644 gogen-api/README.md create mode 100644 gogen-api/api/db_utils.py create mode 100644 gogen-api/api/get.py create mode 100644 gogen-api/api/list.py create mode 100644 gogen-api/api/logger.py create mode 100644 gogen-api/api/s3_utils.py create mode 100644 gogen-api/api/search.py create mode 100644 gogen-api/api/upsert.py create mode 100644 gogen-api/backup_restore.py create mode 100644 gogen-api/create_local_table.py create mode 100755 gogen-api/deploy_lambdas.sh create mode 100644 gogen-api/docker-compose.yml delete mode 100644 gogen-api/get.py create mode 100644 gogen-api/get_schema.py delete mode 100644 gogen-api/list.py create mode 100644 gogen-api/requirements.txt delete mode 100644 gogen-api/search.py create mode 100755 gogen-api/setup_local_db.sh create mode 100755 gogen-api/start_dev.sh create mode 100755 gogen-api/stop_dev.sh create mode 100644 gogen-api/table_schema.json create mode 100644 gogen-api/template.yaml create mode 100644 gogen-api/test_dynamodb.py create mode 100644 gogen-api/test_s3.py delete mode 100644 gogen-api/upsert.py create mode 100755 setup_venv.sh diff --git a/.cursor/rules/gogen-api.mdc b/.cursor/rules/gogen-api.mdc new file mode 100644 index 0000000..71c7923 --- /dev/null +++ b/.cursor/rules/gogen-api.mdc @@ -0,0 +1,16 @@ +--- +description: These rules govern work with the api +globs: gogen-api/**/*.py +alwaysApply: false +--- +# General Development Rules + +- Keep components modular and focused on a single responsibility +- Document code with clear comments +- Update gogen-api/SUMMARY.md after completing significant features + +## File Organization + +- Each AWS Lambda function is a separate .py file in the `gogen-api` directory. +- Files were originally written in python2.7 but need to be updated to python3. +- APIs were originally implemented in 2017 and need to be updated to 2025 APIs. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2953ec6..062faf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ main ] + branches: [ master ] pull_request: jobs: @@ -39,7 +39,7 @@ jobs: if: github.ref == 'refs/heads/master' run: docker build -t clintsharp/gogen . - # Deployment: These steps run only on the main branch. + # Deployment: These steps run only on the master branch. - name: Configure AWS Credentials if: github.ref == 'refs/heads/master' uses: aws-actions/configure-aws-credentials@v1 @@ -54,4 +54,43 @@ jobs: - name: Run Docker Push Script if: github.ref == 'refs/heads/master' - run: bash docker-push.sh \ No newline at end of file + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: bash docker-push.sh + + deploy-lambdas: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/master' + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.13' + + - name: Set up Python virtual environment + run: | + python -m pip install --upgrade pip + python -m venv .pyvenv + source .pyvenv/bin/activate + pip install boto3 botocore awscli + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-1 + + - name: Deploy Lambda Functions + env: + LAMBDA_ROLE_ARN: ${{ secrets.LAMBDA_ROLE_ARN }} + run: | + source .pyvenv/bin/activate + cd gogen-api + # Run the deployment script + ./deploy_lambdas.sh diff --git a/.gitignore b/.gitignore index 0bbb247..46d0879 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,8 @@ splunk_app_gogen.spl .configcache* .versioncache* roveralls* +.specstory +.pyvenv +gogen-api/__pycache__ +gogen-api/build +ui/* \ No newline at end of file diff --git a/README.md b/README.md index bc29971..82b2e59 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Seems a nice user named Coccyx has published a weblog configuration for us. Let gogen info coccyx/weblog -As we see from the info, the actual configuration is stored in a GitHub gist. Feel free to click through the link in the info to see the full configuration. +The full configuration is stored in S3 behind the Gogen API. We can retrieve the configuration with `gogen -c coccyx/weblog config`. Let's generate a few weblog entries. gogen -c coccyx/weblog diff --git a/gogen-api/Dockerfile b/gogen-api/Dockerfile new file mode 100644 index 0000000..f903d7d --- /dev/null +++ b/gogen-api/Dockerfile @@ -0,0 +1,13 @@ +FROM public.ecr.aws/lambda/python:3.13 + +# Copy requirements.txt +COPY requirements.txt ${LAMBDA_TASK_ROOT} + +# Install the specified packages +RUN pip install -r requirements.txt -t ${LAMBDA_TASK_ROOT} + +# Copy function code +COPY api/ ${LAMBDA_TASK_ROOT}/ + +# Set the CMD to your handler +CMD [ "get.lambda_handler" ] \ No newline at end of file diff --git a/gogen-api/README.md b/gogen-api/README.md new file mode 100644 index 0000000..b253108 --- /dev/null +++ b/gogen-api/README.md @@ -0,0 +1,310 @@ +# Gogen API Local Development Environment + +This document describes how to set up and use the local development environment for the Gogen API. + +## Overview + +The local development environment includes: + +- **DynamoDB Local**: A local version of AWS DynamoDB for data storage +- **MinIO**: A local S3-compatible object storage server for the `gogen-configs` bucket +- **SAM Local**: AWS Serverless Application Model for local Lambda function development + +This setup allows you to develop and test the entire Gogen API stack without needing access to AWS services. + +## Prerequisites + +- Docker and Docker Compose +- AWS SAM CLI +- Python 3.13 or compatible version +- AWS CLI (optional, for advanced testing) + +## Python Virtual Environment Setup + +The project uses a Python virtual environment located at `.pyvenv` in the root directory. This keeps dependencies isolated and ensures consistent development across different machines. + +### Creating the Virtual Environment + +If the `.pyvenv` directory doesn't exist, create it with the following commands: + +```bash +# Navigate to the project root +cd /home/clint/local/src/gogen + +# Create the virtual environment +python3 -m venv .pyvenv + +# Activate the virtual environment +source .pyvenv/bin/activate + +# Install required packages +cd gogen-api +pip install -r requirements.txt +pip install -r requirements-dev.txt # If it exists +``` + +### Using the Virtual Environment with SAM + +To use the virtual environment with SAM: + +1. Activate the virtual environment: + ```bash + source /home/clint/local/src/gogen/.pyvenv/bin/activate + ``` + +2. Configure SAM to use the virtual environment by adding this to your `samconfig.toml` or when running SAM commands: + ```bash + sam build --use-container + # or + sam local invoke --env-vars env.json + ``` + +3. For development, you can add this to your `.bashrc` or `.zshrc` to automatically activate the environment when entering the project directory: + ```bash + function cd() { + builtin cd "$@" + if [[ -d .pyvenv ]] && [[ -f .pyvenv/bin/activate ]]; then + source .pyvenv/bin/activate + fi + } + ``` + +### Updating Dependencies + +When dependencies change, update your virtual environment: + +```bash +source /home/clint/local/src/gogen/.pyvenv/bin/activate +pip install -r gogen-api/requirements.txt +``` + +## Starting the Development Environment + +The easiest way to start the entire development environment is to use the provided script: + +```bash +cd gogen-api +./start_dev.sh +``` + +This script will: +1. Start the Docker containers (DynamoDB and MinIO) using Docker Compose +2. Set up the local DynamoDB with the correct schema +3. Build the SAM application +4. Start the SAM local API on port 4000 +5. Clean up resources when you exit (Ctrl+C) + +## Manual Setup (if needed) + +If you prefer to start services individually: + +1. Start the Docker containers: + ```bash + docker-compose up -d + ``` + +2. Set up the local DynamoDB: + ```bash + ./setup_local_db.sh + ``` + +3. Build and start the SAM application: + ```bash + # Make sure your virtual environment is activated + source /home/clint/local/src/gogen/.pyvenv/bin/activate + + # Build and start SAM + sam build + sam local start-api --host 0.0.0.0 --port 4000 --warm-containers EAGER --docker-network lambda-local + ``` + +## Testing the Environment + +### Testing DynamoDB + +A test script is provided to verify the DynamoDB connection: + +```bash +# Activate virtual environment if not already active +source /home/clint/local/src/gogen/.pyvenv/bin/activate + +python test_dynamodb.py +``` + +### Testing S3/MinIO + +A test script is provided to verify the S3 connection: + +```bash +# Activate virtual environment if not already active +source /home/clint/local/src/gogen/.pyvenv/bin/activate + +python test_s3.py +``` + +This script will: +1. Connect to the local MinIO server +2. List available buckets +3. Upload a test file to the `gogen-configs` bucket +4. List objects in the bucket +5. Download and verify the test file + +## Accessing Services + +### API Endpoints + +The SAM local API is available at: http://localhost:4000 + +Available endpoints: +- GET /v1/get/{gogen} - Get a specific Gogen configuration +- POST /v1/upsert - Create or update a Gogen configuration +- GET /v1/list - List all available Gogen configurations + +### DynamoDB Local + +DynamoDB Local is available at: http://localhost:8000 + +### MinIO (S3) + +#### Web Console + +You can access the MinIO web console at: http://localhost:9001 + +Login with: +- Username: `minioadmin` +- Password: `minioadmin` + +#### API + +The MinIO S3 API is available at: http://localhost:9000 + +## Using Services in Your Code + +### DynamoDB + +Use the provided utility module for DynamoDB operations: + +```python +from api.db_utils import get_dynamodb_client + +# Get DynamoDB client +dynamodb = get_dynamodb_client() +table = dynamodb.Table('gogen') + +# Perform operations +response = table.get_item(Key={'gogen': 'my-gogen'}) +``` + +### S3/MinIO + +Use the provided utility module for S3 operations: + +```python +from api.s3_utils import upload_config, download_config, list_configs, delete_config + +# Upload a config +upload_config('my-config.json', '{"key": "value"}') + +# Download a config +content = download_config('my-config.json') + +# List all configs +configs = list_configs() + +# Delete a config +delete_config('my-config.json') +``` + +## AWS CLI with Local Services + +### DynamoDB + +```bash +aws dynamodb list-tables --endpoint-url http://localhost:8000 +aws dynamodb scan --table-name gogen --endpoint-url http://localhost:8000 +``` + +### S3/MinIO + +You can use the AWS CLI with MinIO by creating a profile: + +```bash +aws configure --profile minio +``` + +Enter: +- AWS Access Key ID: `minioadmin` +- AWS Secret Access Key: `minioadmin` +- Default region name: `us-east-1` +- Default output format: `json` + +Then use the profile with the endpoint URL: + +```bash +aws --endpoint-url http://localhost:9000 --profile minio s3 ls +aws --endpoint-url http://localhost:9000 --profile minio s3 ls s3://gogen-configs +``` + +## Connecting from Docker Containers + +When connecting to services from other Docker containers in the same network: +- Use `dynamodb-local:8000` instead of `localhost:8000` for DynamoDB +- Use `minio:9000` instead of `localhost:9000` for S3/MinIO + +## Data Persistence + +- DynamoDB data is persisted in the container +- MinIO data is persisted in a Docker volume named `minio-data` + +This ensures your data is preserved between container restarts. + +## Troubleshooting + +### General Issues + +If you encounter issues with the development environment: + +1. Check if all containers are running: + ```bash + docker-compose ps + ``` + +2. Check the logs: + ```bash + docker-compose logs + ``` + +3. Restart the services: + ```bash + docker-compose restart + ``` + +4. If all else fails, recreate the services: + ```bash + docker-compose down + docker-compose up -d + ./setup_local_db.sh + ``` + +### DynamoDB Issues + +If DynamoDB is not working correctly: +```bash +docker-compose logs dynamodb-local +``` + +### S3/MinIO Issues + +If MinIO is not working correctly: +```bash +docker-compose logs minio +docker-compose logs createbuckets +``` + +## Development Guidelines + +- Keep components modular and focused on a single responsibility +- Document code with clear comments +- Update SUMMARY.md after completing significant features +- Each AWS Lambda function should be a separate .py file in the `api` directory +- Remember that the codebase is being updated from Python 2.7 to Python 3.13 \ No newline at end of file diff --git a/gogen-api/api/db_utils.py b/gogen-api/api/db_utils.py new file mode 100644 index 0000000..c2faa24 --- /dev/null +++ b/gogen-api/api/db_utils.py @@ -0,0 +1,39 @@ +import os +import boto3 +from botocore.config import Config +from logger import setup_logger + +logger = setup_logger(__name__) + +def get_dynamodb_client(): + """ + Get a DynamoDB client - uses local endpoint if running locally + """ + if os.environ.get('AWS_SAM_LOCAL'): + # Use the container name as hostname when running in SAM Lambda + logger.info("Configuring DynamoDB client for local development") + config = Config( + connect_timeout=5, + read_timeout=5, + retries={'max_attempts': 2}, + max_pool_connections=10, + tcp_keepalive=True + ) + logger.debug(f"Using config: {config}") + + client = boto3.resource('dynamodb', + endpoint_url='http://dynamodb-local:8000', + region_name='us-east-1', + aws_access_key_id='DUMMYIDEXAMPLE', + aws_secret_access_key='DUMMYEXAMPLEKEY', + config=config) + # Test the connection + try: + logger.info("Testing DynamoDB connection...") + tables = client.meta.client.list_tables() + logger.info(f"Connection successful. Available tables: {tables['TableNames']}") + except Exception as e: + logger.error(f"Failed to connect to local DynamoDB: {str(e)}") + raise + return client + return boto3.resource('dynamodb') \ No newline at end of file diff --git a/gogen-api/api/get.py b/gogen-api/api/get.py new file mode 100644 index 0000000..43fc20e --- /dev/null +++ b/gogen-api/api/get.py @@ -0,0 +1,188 @@ +import json +import decimal +import urllib.request +import urllib.error +from boto3.dynamodb.conditions import Key, Attr +from db_utils import get_dynamodb_client +from s3_utils import download_config +from logger import setup_logger + +logger = setup_logger(__name__) +logger.info('Loading function') + + +def decimal_default(obj): + if isinstance(obj, decimal.Decimal): + return float(obj) + raise TypeError + + +def respond(err, res=None): + return { + 'statusCode': '400' if err else '200', + 'body': str(err) if err else json.dumps(res, default=decimal_default), + 'headers': { + 'Content-Type': 'application/json', + }, + } + + +def fetch_gist_content(gist_id): + try: + # Use GitHub API to get gist content + api_url = f'https://api.github.com/gists/{gist_id}' + logger.info(f"Fetching gist from GitHub API: {api_url}") + + # Create a request with headers + headers = { + 'User-Agent': 'Gogen-API-Lambda/1.0', + 'Accept': 'application/vnd.github.v3+json' + } + req = urllib.request.Request(api_url, headers=headers) + + # Set a timeout to avoid hanging + logger.debug("Opening URL connection with timeout") + + # Wrap each step in its own try-except block for detailed error tracking + try: + connection = urllib.request.urlopen(req, timeout=5) + logger.debug("Connection established successfully") + except Exception as conn_error: + logger.error(f"Error establishing connection: {str(conn_error)}") + import traceback + logger.error(f"Connection traceback: {traceback.format_exc()}") + return None + + try: + with connection as response: + logger.debug(f"Connection opened. Reading response data") + try: + response_data = response.read() + logger.debug(f"Response data read. Length: {len(response_data)} bytes") + except Exception as read_error: + logger.error(f"Error reading response data: {str(read_error)}") + import traceback + logger.error(f"Read traceback: {traceback.format_exc()}") + return None + + try: + gist_data = json.loads(response_data.decode('utf-8')) + logger.info(f"Successfully fetched gist data. Status: {response.status}") + except Exception as json_error: + logger.error(f"Error decoding JSON: {str(json_error)}") + logger.error(f"Raw response data: {response_data[:200]}...") # Log first 200 chars + import traceback + logger.error(f"JSON decode traceback: {traceback.format_exc()}") + return None + + # Get the first file's content + try: + if not gist_data.get('files'): + logger.error("No files found in gist") + logger.error(f"Gist data keys: {list(gist_data.keys())}") + return None + + # Get the first file's content + first_file = next(iter(gist_data['files'].values())) + content = first_file.get('content') + + if not content: + logger.error("No content found in gist file") + logger.error(f"First file keys: {list(first_file.keys())}") + return None + + logger.info(f"Successfully extracted content. Length: {len(content)} bytes") + return content + except Exception as extract_error: + logger.error(f"Error extracting content from gist data: {str(extract_error)}") + logger.error(f"Gist data structure: {str(gist_data)[:200]}...") # Log first 200 chars + import traceback + logger.error(f"Content extraction traceback: {traceback.format_exc()}") + return None + except Exception as with_error: + logger.error(f"Error in 'with' context manager: {str(with_error)}") + import traceback + logger.error(f"With context traceback: {traceback.format_exc()}") + return None + + except urllib.error.URLError as e: + logger.error(f"URLError fetching gist: {str(e)}") + if hasattr(e, 'code'): + logger.error(f"HTTP Error Code: {e.code}") + if hasattr(e, 'reason'): + logger.error(f"Error Reason: {e.reason}") + if hasattr(e, 'headers'): + logger.debug(f"Error Response Headers: {dict(e.headers)}") + import traceback + logger.error(f"URLError traceback: {traceback.format_exc()}") + return None + except json.JSONDecodeError as e: + logger.error(f"JSONDecodeError from GitHub response: {str(e)}") + import traceback + logger.error(f"JSONDecodeError traceback: {traceback.format_exc()}") + return None + except Exception as e: + logger.error(f"Unexpected error while fetching gist: {str(e)}") + import traceback + logger.error(f"General exception traceback: {traceback.format_exc()}") + return None + + +def lambda_handler(event, context): + logger.debug(f"Received event: {json.dumps(event)}") + q = event['pathParameters']['proxy'] + logger.debug(f"Query: {q}") + + table = get_dynamodb_client().Table('gogen') + response = table.get_item(Key={"gogen": q}) + + if 'Item' not in response: + logger.error(f"No item found for query: {q}") + return { + 'statusCode': '404', + 'body': f'Could not find Gogen: {q}', + } + + item = response['Item'] + if 'gogen' not in item: + logger.error(f"Item found but missing 'gogen' key for query: {q}") + return { + 'statusCode': '404', + 'body': f'Could not find Gogen: {q}', + } + + # Try to fetch the configuration from S3 first + if 's3Path' in item: + logger.debug(f"Found s3Path: {item['s3Path']} for query: {q}") + config_content = download_config(item['s3Path']) + if config_content: + item['config'] = config_content + logger.debug(f"Successfully added config content from S3 for query: {q}") + else: + logger.error(f"Failed to fetch config content from S3 for query: {q}") + return { + 'statusCode': '500', + 'body': f'Failed to fetch configuration from S3 for: {q}', + } + # For backward compatibility, try to fetch from GitHub gist if s3Path is not present + elif 'gistID' in item: + logger.warning(f"Using legacy gistID: {item['gistID']} for query: {q}. This will be deprecated.") + config_content = fetch_gist_content(item['gistID']) + if config_content: + item['config'] = config_content + logger.debug(f"Successfully added config content from GitHub gist for query: {q}") + else: + logger.error(f"Failed to fetch config content from GitHub gist for query: {q}") + return { + 'statusCode': '500', + 'body': f'Failed to fetch configuration from GitHub gist for: {q}', + } + else: + logger.error(f"No s3Path or gistID found in item for query: {q}") + return { + 'statusCode': '500', + 'body': f'Configuration {q} does not have a valid storage location.', + } + + response['Item'] = item + return respond(None, response) diff --git a/gogen-api/api/list.py b/gogen-api/api/list.py new file mode 100644 index 0000000..930b6c9 --- /dev/null +++ b/gogen-api/api/list.py @@ -0,0 +1,55 @@ +import json +from db_utils import get_dynamodb_client +from logger import setup_logger + +logger = setup_logger(__name__) +logger.info('Loading function') + +def respond(err, res=None): + return { + 'statusCode': '400' if err else '200', + 'body': str(err) if err else json.dumps(res), + 'headers': { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + } + +def lambda_handler(event, context): + try: + logger.debug(f"Received event: {json.dumps(event, indent=2)}") + table = get_dynamodb_client().Table('gogen') + + # Use pagination to handle large datasets + items = [] + last_evaluated_key = None + page_count = 0 + + while True: + scan_kwargs = { + 'ProjectionExpression': "gogen, description", + 'Limit': 20 + } + + if last_evaluated_key: + scan_kwargs['ExclusiveStartKey'] = last_evaluated_key + + logger.debug(f"Scanning DynamoDB table with kwargs: {scan_kwargs}") + response = table.scan(**scan_kwargs) + + page_items = response.get('Items', []) + items.extend(page_items) + page_count += 1 + logger.debug(f"Retrieved {len(page_items)} items on page {page_count}") + + last_evaluated_key = response.get('LastEvaluatedKey') + if not last_evaluated_key: + break + logger.debug(f"More pages available, continuing scan with key: {last_evaluated_key}") + + logger.info(f"Successfully retrieved {len(items)} total items across {page_count} pages") + return respond(None, {'Items': items}) + + except Exception as e: + logger.error(f"Error in lambda_handler: {str(e)}", exc_info=True) + return respond(e) \ No newline at end of file diff --git a/gogen-api/api/logger.py b/gogen-api/api/logger.py new file mode 100644 index 0000000..2e2a9df --- /dev/null +++ b/gogen-api/api/logger.py @@ -0,0 +1,32 @@ +import logging +import sys +import os + +def setup_logger(name): + """ + Set up a logger with consistent formatting and configuration + """ + # Create logger + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + # Only add handler if logger doesn't already have handlers + if not logger.handlers: + # Create console handler + handler = logging.StreamHandler(sys.stdout) + + # Create formatter + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Add formatter to handler + handler.setFormatter(formatter) + + # Add handler to logger + logger.addHandler(handler) + + # Prevent propagation to avoid duplicate logs + logger.propagate = False + + return logger \ No newline at end of file diff --git a/gogen-api/api/s3_utils.py b/gogen-api/api/s3_utils.py new file mode 100644 index 0000000..4a3cd36 --- /dev/null +++ b/gogen-api/api/s3_utils.py @@ -0,0 +1,160 @@ +import os +import boto3 +from botocore.config import Config +from logger import setup_logger + +logger = setup_logger(__name__) + +def get_s3_client(): + """ + Get an S3 client - uses local endpoint if running locally, otherwise uses AWS credentials + """ + # Common config for both local and production environments + config = Config( + connect_timeout=5, + read_timeout=5, + retries={'max_attempts': 3}, + max_pool_connections=10, + tcp_keepalive=True + ) + + if os.environ.get('AWS_SAM_LOCAL'): + # Use the container name as hostname when running in SAM Lambda + logger.info("Configuring S3 client for local development") + + client = boto3.resource('s3', + endpoint_url='http://minio:9000', # Use container name in docker network + region_name='us-east-1', + aws_access_key_id='minioadmin', + aws_secret_access_key='minioadmin', + config=config) + # Test the connection + try: + logger.info("Testing S3 connection...") + buckets = list(client.buckets.all()) + logger.info(f"Connection successful. Available buckets: {[b.name for b in buckets]}") + except Exception as e: + logger.error(f"Failed to connect to local S3: {str(e)}") + raise + return client + else: + # Production environment - use AWS credentials from environment or instance profile + logger.info("Configuring S3 client for production environment") + region = os.environ.get('AWS_REGION', 'us-east-1') + try: + client = boto3.resource('s3', region_name=region, config=config) + logger.info(f"Successfully created S3 client for region {region}") + return client + except Exception as e: + logger.error(f"Failed to create production S3 client: {str(e)}") + raise + +def get_config_bucket(): + """ + Get the gogen-configs bucket + """ + try: + s3 = get_s3_client() + bucket_name = os.environ.get('CONFIG_BUCKET_NAME', 'gogen-configs') + bucket = s3.Bucket(bucket_name) + + try: + # Verify bucket exists by trying to load bucket properties + bucket.meta.client.head_bucket(Bucket=bucket_name) + logger.info(f"Successfully connected to bucket: {bucket_name}") + except bucket.meta.client.exceptions.ClientError as e: + error_code = e.response['Error']['Code'] + if error_code == '403': + logger.error(f"Access denied (403) to bucket {bucket_name}. Check IAM permissions.") + logger.error("Required permissions: s3:ListBucket, s3:GetBucketLocation, s3:GetObject, s3:PutObject, s3:DeleteObject") + elif error_code == '404': + logger.error(f"Bucket {bucket_name} not found (404). Check if the bucket exists.") + else: + logger.error(f"Error accessing bucket {bucket_name}: {error_code}") + raise + + return bucket + except Exception as e: + logger.error(f"Error accessing config bucket: {str(e)}") + raise + +def upload_config(config_name, content): + """ + Upload a config file to the gogen-configs bucket + + Args: + config_name (str): Name of the config file + content (str): Content of the config file + + Returns: + bool: True if successful, False otherwise + """ + try: + bucket = get_config_bucket() + bucket.put_object(Key=config_name, Body=content) + logger.info(f"Successfully uploaded config {config_name}") + return True + except Exception as e: + logger.error(f"Error uploading config {config_name}: {str(e)}") + return False + +def download_config(config_name): + """ + Download a config file from the gogen-configs bucket + + Args: + config_name (str): Name of the config file + + Returns: + str: Content of the config file, or None if not found + """ + try: + bucket = get_config_bucket() + obj = bucket.Object(config_name) + content = obj.get()['Body'].read().decode('utf-8') + logger.info(f"Successfully downloaded config {config_name}") + return content + except bucket.meta.client.exceptions.NoSuchKey: + logger.warning(f"Config {config_name} not found") + return None + except Exception as e: + logger.error(f"Error downloading config {config_name}: {str(e)}") + return None + +def list_configs(): + """ + List all configs in the gogen-configs bucket + + Returns: + list: List of config names + """ + try: + bucket = get_config_bucket() + configs = [obj.key for obj in bucket.objects.all()] + logger.info(f"Found {len(configs)} configs") + return configs + except Exception as e: + logger.error(f"Error listing configs: {str(e)}") + return [] + +def delete_config(config_name): + """ + Delete a config file from the gogen-configs bucket + + Args: + config_name (str): Name of the config file + + Returns: + bool: True if successful, False otherwise + """ + try: + bucket = get_config_bucket() + bucket.Object(config_name).delete() + logger.info(f"Successfully deleted config {config_name}") + return True + except bucket.meta.client.exceptions.NoSuchKey: + logger.warning(f"Config {config_name} not found for deletion") + return False + except Exception as e: + logger.error(f"Error deleting config {config_name}: {str(e)}") + return False \ No newline at end of file diff --git a/gogen-api/api/search.py b/gogen-api/api/search.py new file mode 100644 index 0000000..0e13c5f --- /dev/null +++ b/gogen-api/api/search.py @@ -0,0 +1,70 @@ +import json +from boto3.dynamodb.conditions import Key, Attr +from db_utils import get_dynamodb_client +from logger import setup_logger + +logger = setup_logger(__name__) +logger.info('Loading function') + + +def respond(err, res=None): + return { + 'statusCode': '400' if err else '200', + 'body': str(err) if err else json.dumps(res), + 'headers': { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + } + + +def lambda_handler(event, context): + try: + logger.debug(f"Received event: {json.dumps(event, indent=2)}") + + # Get search query from parameters + query_params = event.get('queryStringParameters', {}) + q = query_params.get('q') + + if not q: + logger.warning("No search query provided") + return respond("Search query parameter 'q' is required") + + logger.info(f"Processing search query: {q}") + + table = get_dynamodb_client().Table('gogen') + + # Use pagination to handle large datasets + items = [] + last_evaluated_key = None + page_count = 0 + + while True: + scan_kwargs = { + 'ProjectionExpression': "gogen, description", + 'FilterExpression': Attr("gogen").contains(q) | Attr("description").contains(q), + 'Limit': 20 + } + + if last_evaluated_key: + scan_kwargs['ExclusiveStartKey'] = last_evaluated_key + + logger.debug(f"Scanning DynamoDB table with kwargs: {scan_kwargs}") + response = table.scan(**scan_kwargs) + + page_items = response.get('Items', []) + items.extend(page_items) + page_count += 1 + logger.debug(f"Retrieved {len(page_items)} matching items on page {page_count}") + + last_evaluated_key = response.get('LastEvaluatedKey') + if not last_evaluated_key: + break + logger.debug(f"More pages available, continuing scan with key: {last_evaluated_key}") + + logger.info(f"Successfully retrieved {len(items)} total matching items across {page_count} pages") + return respond(None, {'Items': items}) + + except Exception as e: + logger.error(f"Error in lambda_handler: {str(e)}", exc_info=True) + return respond(e) \ No newline at end of file diff --git a/gogen-api/api/upsert.py b/gogen-api/api/upsert.py new file mode 100644 index 0000000..84d8c50 --- /dev/null +++ b/gogen-api/api/upsert.py @@ -0,0 +1,129 @@ +import json +import http.client +from boto3.dynamodb.conditions import Key, Attr +from db_utils import get_dynamodb_client +from s3_utils import upload_config +from logger import setup_logger + +logger = setup_logger(__name__) +logger.info('Loading function') + + +def respond(err, res=None): + return { + 'statusCode': '400' if err else '200', + 'body': str(err) if err else json.dumps(res), + 'headers': { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + } + + +def validate_github_token(token): + """ + Validate the GitHub token by making a request to GitHub's API + """ + headers = { + 'Authorization': token, + 'User-Agent': 'gogen lambda', + 'Content-Length': '0' + } + + logger.debug("Attempting to validate GitHub token") + conn = http.client.HTTPSConnection('api.github.com') + conn.request("GET", "/user", None, headers) + response = conn.getresponse() + + if response.status != 200: + data = response.read().decode('utf-8') + logger.error(f"GitHub token validation failed. Status: {response.status}, Reason: {response.reason}") + logger.debug(f"GitHub API response: {data}") + return False, f"Unable to authenticate user to GitHub, status: {response.status}, msg: {response.reason}" + + logger.info("GitHub token validation successful") + return True, None + + +def lambda_handler(event, context): + try: + logger.debug(f"Received event: {json.dumps(event, indent=2)}") + + # Validate request body + if 'body' not in event: + logger.error("No request body provided") + return respond("Request body is required") + + try: + body = json.loads(event['body']) + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON in request body: {str(e)}") + return respond("Invalid JSON in request body") + + # Validate GitHub authorization + if 'headers' not in event or 'Authorization' not in event['headers']: + logger.error("Authorization header not present") + return respond("Authorization header not present") + + # Validate GitHub token + is_valid, error_msg = validate_github_token(event['headers']['Authorization']) + if not is_valid: + return respond(error_msg) + + # Validate and clean request body + validated_body = {} + for k, v in body.items(): + if v != "": + validated_body[k] = v + + if not validated_body: + logger.error("No valid fields in request body") + return respond("No valid fields in request body") + + # Check if config is present in the request + if 'config' in validated_body: + config_content = validated_body['config'] + + # Create S3 path in the format username/sample.yml + if 'owner' in validated_body and 'name' in validated_body: + s3_path = f"{validated_body['owner']}/{validated_body['name']}.yml" + + # Upload config to S3 + logger.info(f"Uploading config to S3 at path: {s3_path}") + upload_success = upload_config(s3_path, config_content) + + if not upload_success: + logger.error(f"Failed to upload config to S3 at path: {s3_path}") + return respond("Failed to upload configuration to S3") + + # Remove config from DynamoDB item to save space + # We'll store the S3 path instead + validated_body.pop('config', None) + + # Add S3 path to DynamoDB item + validated_body['s3Path'] = s3_path + + # Remove gistID if present (for migration) + validated_body.pop('gistID', None) + else: + logger.error("Owner or name missing in request body") + return respond("Owner and name are required fields") + else: + logger.warning("No config found in request body") + + logger.info(f"Processing upsert for item: {validated_body}") + + # Store in DynamoDB + table = get_dynamodb_client().Table('gogen') + logger.debug(f"Attempting to upsert item to DynamoDB: {validated_body}") + + response = table.put_item( + Item=validated_body + ) + + logger.info(f"Successfully upserted item to DynamoDB") + return respond(None, response) + + except Exception as e: + logger.error(f"Error in lambda_handler: {str(e)}", exc_info=True) + return respond(e) \ No newline at end of file diff --git a/gogen-api/backup_restore.py b/gogen-api/backup_restore.py new file mode 100644 index 0000000..dd49831 --- /dev/null +++ b/gogen-api/backup_restore.py @@ -0,0 +1,84 @@ +import boto3 +import json +from decimal import Decimal +import os +from botocore.config import Config + +class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + # Convert Decimal to float if it's a whole number, otherwise keep as string + if obj % 1 == 0: + return int(obj) + return float(obj) + return super(DecimalEncoder, self).default(obj) + +def backup_table(): + """Backup DynamoDB table to a JSON file""" + dynamodb = boto3.resource('dynamodb') + table = dynamodb.Table('gogen') + + items = [] + scan_kwargs = {} + done = False + start_key = None + + while not done: + if start_key: + scan_kwargs['ExclusiveStartKey'] = start_key + response = table.scan(**scan_kwargs) + items.extend(response.get('Items', [])) + start_key = response.get('LastEvaluatedKey', None) + done = start_key is None + + with open('table_backup.json', 'w') as f: + json.dump(items, f, cls=DecimalEncoder, indent=2) + print(f"Backed up {len(items)} items to table_backup.json") + +def restore_table(local=True): + """Restore DynamoDB table from JSON file""" + if local: + config = Config( + connect_timeout=5, + read_timeout=5, + retries={'max_attempts': 3} + ) + dynamodb = boto3.resource('dynamodb', + endpoint_url='http://localhost:8000', + region_name='us-east-1', + aws_access_key_id='DUMMYIDEXAMPLE', + aws_secret_access_key='DUMMYEXAMPLEKEY', + config=config) + else: + dynamodb = boto3.resource('dynamodb') + + table = dynamodb.Table('gogen') + + try: + with open('table_backup.json', 'r') as f: + items = json.load(f) + + with table.batch_writer() as batch: + for item in items: + # Convert numeric strings back to Decimal where appropriate + processed_item = {} + for key, value in item.items(): + if isinstance(value, (int, float)): + processed_item[key] = Decimal(str(value)) + else: + processed_item[key] = value + batch.put_item(Item=processed_item) + + print(f"Restored {len(items)} items to {'local' if local else 'remote'} table") + except Exception as e: + print(f"Error restoring data: {str(e)}") + +if __name__ == '__main__': + # If running locally, make sure the table exists first + if os.environ.get('LOCAL_DYNAMODB'): + from create_local_table import create_local_table + create_local_table() + + # By default, backup from remote and restore to local + backup_table() + restore_table(local=True) \ No newline at end of file diff --git a/gogen-api/create_local_table.py b/gogen-api/create_local_table.py new file mode 100644 index 0000000..4b9817b --- /dev/null +++ b/gogen-api/create_local_table.py @@ -0,0 +1,50 @@ +import json +import boto3 +from botocore.config import Config +from decimal import Decimal +import os + +def create_local_table(): + """Create a table in local DynamoDB using the schema from table_schema.json""" + # Load schema from file + with open('table_schema.json', 'r') as f: + schema = json.load(f) + + # Configure local DynamoDB client + config = Config( + connect_timeout=5, + read_timeout=5, + retries={'max_attempts': 3} + ) + dynamodb = boto3.resource('dynamodb', + endpoint_url='http://localhost:8000', + region_name='us-east-1', + aws_access_key_id='DUMMYIDEXAMPLE', + aws_secret_access_key='DUMMYEXAMPLEKEY', + config=config) + + # Create table using schema + try: + table = dynamodb.create_table( + TableName=schema['TableName'], + KeySchema=schema['KeySchema'], + AttributeDefinitions=schema['AttributeDefinitions'], + ProvisionedThroughput=schema['ProvisionedThroughput'] + ) + + # Add GSIs if they exist in the schema + if 'GlobalSecondaryIndexes' in schema: + # GSIs are added during table creation, so we don't need to do anything here + pass + + # Wait for the table to be created + table.meta.client.get_waiter('table_exists').wait(TableName=schema['TableName']) + print('Table created successfully!') + except Exception as e: + if 'ResourceInUseException' in str(e): + print('Table already exists!') + else: + print(f'Error creating table: {str(e)}') + +if __name__ == '__main__': + create_local_table() \ No newline at end of file diff --git a/gogen-api/deploy_lambdas.sh b/gogen-api/deploy_lambdas.sh new file mode 100755 index 0000000..3035bf6 --- /dev/null +++ b/gogen-api/deploy_lambdas.sh @@ -0,0 +1,143 @@ +#!/bin/bash +set -e + +# Determine script directory and project root +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" + +# Configuration +LAMBDA_DIR="$SCRIPT_DIR/api" +BUILD_DIR="$SCRIPT_DIR/build" +REGION="us-east-1" # Change this to your AWS region +RUNTIME="python3.13" + +# Use environment variable if set, otherwise use the default value +ROLE_ARN=${LAMBDA_ROLE_ARN:-$ROLE_ARN} + +# Check if virtual environment exists and activate it +VENV_PATH="$PROJECT_ROOT/.pyvenv" +if [ -d "$VENV_PATH" ]; then + echo "Activating Python virtual environment..." + source "$VENV_PATH/bin/activate" +else + echo "Python virtual environment not found at $VENV_PATH" + echo "Setting up virtual environment..." + + # Check if setup_venv.sh exists and run it + if [ -f "$PROJECT_ROOT/setup_venv.sh" ]; then + echo "Running setup_venv.sh..." + (cd "$PROJECT_ROOT" && bash setup_venv.sh) + source "$VENV_PATH/bin/activate" + else + echo "Creating virtual environment manually..." + python3 -m venv "$VENV_PATH" + source "$VENV_PATH/bin/activate" + pip install --upgrade pip + + # Install requirements if they exist + if [ -f "$SCRIPT_DIR/requirements.txt" ]; then + echo "Installing requirements from $SCRIPT_DIR/requirements.txt..." + pip install -r "$SCRIPT_DIR/requirements.txt" + else + echo "Installing boto3 and botocore..." + pip install boto3 botocore + fi + + # Install AWS CLI if needed + if ! command -v aws &> /dev/null; then + echo "Installing AWS CLI..." + pip install awscli + fi + fi +fi + +# Create build directory if it doesn't exist +mkdir -p $BUILD_DIR + +# Function to package and deploy a Lambda function +deploy_lambda() { + local function_name="Gogen$1" + local handler_file="${1,,}.py" # Convert to lowercase + local handler_name="${1,,}.lambda_handler" + local zip_file="$BUILD_DIR/${function_name}.zip" + + echo "Packaging $function_name..." + + # Create a temporary directory for packaging + local temp_dir=$(mktemp -d) + + # Copy the handler file and dependencies + cp "$LAMBDA_DIR/$handler_file" "$temp_dir/" + cp "$LAMBDA_DIR/db_utils.py" "$temp_dir/" + cp "$LAMBDA_DIR/logger.py" "$temp_dir/" + + # Copy s3_utils.py if needed by this function + if [[ "$1" == "Get" || "$1" == "Upsert" ]]; then + cp "$LAMBDA_DIR/s3_utils.py" "$temp_dir/" + fi + + # Install dependencies into the package + if [ -f "$SCRIPT_DIR/requirements.txt" ]; then + echo "Installing dependencies from requirements.txt..." + pip install -r "$SCRIPT_DIR/requirements.txt" -t "$temp_dir/" --no-cache-dir + else + echo "requirements.txt not found, installing boto3 and botocore..." + pip install boto3 botocore -t "$temp_dir/" --no-cache-dir + fi + + # Create zip file + echo "Creating zip file: $zip_file" + (cd "$temp_dir" && zip -r "$zip_file" .) + + # Check if Lambda function exists + echo "Checking if Lambda function $function_name exists..." + if aws lambda get-function --function-name "$function_name" --region "$REGION" 2>&1 | grep -q "Function not found"; then + # Create new Lambda function + echo "Creating new Lambda function: $function_name" + aws lambda create-function \ + --function-name "$function_name" \ + --runtime "$RUNTIME" \ + --role "$ROLE_ARN" \ + --handler "$handler_name" \ + --zip-file "fileb://$zip_file" \ + --region "$REGION" + else + # Update existing Lambda function + echo "Updating existing Lambda function: $function_name" + aws lambda update-function-code \ + --function-name "$function_name" \ + --zip-file "fileb://$zip_file" \ + --region "$REGION" + fi + + # Clean up + rm -rf "$temp_dir" + + echo "$function_name deployment complete!" +} + +# Validate AWS CLI is installed +if ! command -v aws &> /dev/null; then + echo "AWS CLI is not installed. Installing..." + pip install awscli +fi + +# Validate AWS credentials are configured +if ! aws sts get-caller-identity &> /dev/null; then + echo "AWS credentials are not configured. Please run 'aws configure' first." + exit 1 +fi + +# Check if ROLE_ARN is set +if [ -z "$ROLE_ARN" ]; then + echo "Please set the LAMBDA_ROLE_ARN environment variable or the ROLE_ARN variable in this script." + exit 1 +fi + +# Deploy each Lambda function +deploy_lambda "Get" +deploy_lambda "List" +deploy_lambda "Search" +deploy_lambda "Upsert" + +echo "All Lambda functions deployed successfully!" \ No newline at end of file diff --git a/gogen-api/docker-compose.yml b/gogen-api/docker-compose.yml new file mode 100644 index 0000000..4f5ef39 --- /dev/null +++ b/gogen-api/docker-compose.yml @@ -0,0 +1,48 @@ +version: '3.8' +services: + dynamodb-local: + image: amazon/dynamodb-local:latest + container_name: dynamodb-local + ports: + - "8000:8000" + command: "-jar DynamoDBLocal.jar -sharedDb" + networks: + - lambda-local + + minio: + image: minio/minio:latest + container_name: minio-local + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + command: server /data --console-address ":9001" + volumes: + - minio-data:/data + networks: + - lambda-local + + createbuckets: + image: minio/mc + depends_on: + - minio + entrypoint: > + /bin/sh -c " + sleep 5; + /usr/bin/mc config host add myminio http://minio:9000 minioadmin minioadmin; + /usr/bin/mc mb myminio/gogen-configs; + /usr/bin/mc policy set public myminio/gogen-configs; + exit 0; + " + networks: + - lambda-local + +networks: + lambda-local: + name: lambda-local + driver: bridge + +volumes: + minio-data: diff --git a/gogen-api/get.py b/gogen-api/get.py deleted file mode 100644 index d8eb3c2..0000000 --- a/gogen-api/get.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import print_function - -import boto3 -import json -import decimal - -from boto3.dynamodb.conditions import Key, Attr - -print('Loading function') - - -def decimal_default(obj): - if isinstance(obj, decimal.Decimal): - return float(obj) - raise TypeError - - -def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': err.message if err else json.dumps(res, default=decimal_default), - 'headers': { - 'Content-Type': 'application/json', - }, - } - - -def lambda_handler(event, context): - print("Received event: " + json.dumps(event, indent=2)) - q = event['pathParameters']['proxy'] - print("Query: ", q) - - table = boto3.resource('dynamodb').Table('gogen') - response = table.get_item(Key={"gogen": q}) - - # print("Response: " + json.dumps(response, indent=2)) - if 'Item' not in response: - return { - 'statusCode': '404', - 'body': 'Could not find Gogen: %s' % q, - } - if 'gogen' not in response["Item"]: - return { - 'statusCode': '404', - 'body': 'Could not find Gogen: %s' % q, - } - return respond(None, response) diff --git a/gogen-api/get_schema.py b/gogen-api/get_schema.py new file mode 100644 index 0000000..9a676ea --- /dev/null +++ b/gogen-api/get_schema.py @@ -0,0 +1,35 @@ +import boto3 +import json + +def get_table_schema(): + dynamodb = boto3.client('dynamodb') + + try: + response = dynamodb.describe_table(TableName='gogen') + schema = { + 'TableName': response['Table']['TableName'], + 'KeySchema': response['Table']['KeySchema'], + 'AttributeDefinitions': response['Table']['AttributeDefinitions'], + 'ProvisionedThroughput': { + 'ReadCapacityUnits': response['Table']['ProvisionedThroughput']['ReadCapacityUnits'], + 'WriteCapacityUnits': response['Table']['ProvisionedThroughput']['WriteCapacityUnits'] + } + } + + # Add GSIs if they exist + if 'GlobalSecondaryIndexes' in response['Table']: + schema['GlobalSecondaryIndexes'] = response['Table']['GlobalSecondaryIndexes'] + + # Save schema to file + with open('table_schema.json', 'w') as f: + json.dump(schema, f, indent=2) + print("Schema saved to table_schema.json") + + return schema + + except Exception as e: + print(f"Error getting schema: {str(e)}") + return None + +if __name__ == '__main__': + get_table_schema() \ No newline at end of file diff --git a/gogen-api/list.py b/gogen-api/list.py deleted file mode 100644 index 7a987b5..0000000 --- a/gogen-api/list.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import print_function - -import boto3 -import json - -print('Loading function') - - -def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': err.message if err else json.dumps(res), - 'headers': { - 'Content-Type': 'application/json', - }, - } - - -def lambda_handler(event, context): - print("Received event: " + json.dumps(event, indent=2)) - table = boto3.resource('dynamodb').Table('gogen') - response = table.scan( - ProjectionExpression="gogen, description", - ) - return respond(None, response) \ No newline at end of file diff --git a/gogen-api/requirements.txt b/gogen-api/requirements.txt new file mode 100644 index 0000000..2f2e8e1 --- /dev/null +++ b/gogen-api/requirements.txt @@ -0,0 +1,2 @@ +boto3==1.34.29 +botocore==1.34.29 \ No newline at end of file diff --git a/gogen-api/search.py b/gogen-api/search.py deleted file mode 100644 index 1bf2f7e..0000000 --- a/gogen-api/search.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import print_function - -import boto3 -import json -from boto3.dynamodb.conditions import Key, Attr - -print('Loading function') - - -def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': err.message if err else json.dumps(res), - 'headers': { - 'Content-Type': 'application/json', - }, - } - - -def lambda_handler(event, context): - print("Received event: " + json.dumps(event, indent=2)) - q = event['queryStringParameters']['q'] - print("Query: ",q) - table = boto3.resource('dynamodb').Table('gogen') - response = table.scan( - ProjectionExpression="gogen, description", - FilterExpression=Attr("gogen").contains(q) | Attr("description").contains(q) - ) - return respond(None, response) \ No newline at end of file diff --git a/gogen-api/setup_local_db.sh b/gogen-api/setup_local_db.sh new file mode 100755 index 0000000..5a9b2b9 --- /dev/null +++ b/gogen-api/setup_local_db.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Make sure we're in the right directory +cd "$(dirname "$0")" + +# Check if virtual environment is activated, if not activate it +if [ -z "$VIRTUAL_ENV" ]; then + VENV_PATH="/home/clint/local/src/gogen/.pyvenv" + if [ -d "$VENV_PATH" ]; then + echo "Activating Python virtual environment..." + source "$VENV_PATH/bin/activate" + else + echo "Warning: Python virtual environment not found at $VENV_PATH" + echo "Consider creating it with: python3 -m venv $VENV_PATH" + fi +fi + +# Get the schema +echo "Getting table schema..." +python get_schema.py + +# Create the table in local DynamoDB using the schema +echo "Creating table in local DynamoDB using schema..." +LOCAL_DYNAMODB=true python create_local_table.py + +echo "Local DynamoDB setup complete!" \ No newline at end of file diff --git a/gogen-api/start_dev.sh b/gogen-api/start_dev.sh new file mode 100755 index 0000000..f328e0d --- /dev/null +++ b/gogen-api/start_dev.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Determine script directory and project root +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" + +# Set GOGEN_HOME environment variable +export GOGEN_HOME="$PROJECT_ROOT" +echo "Setting GOGEN_HOME to $GOGEN_HOME" + +# Check if virtual environment exists and activate it +VENV_PATH="$PROJECT_ROOT/.pyvenv" +if [ -d "$VENV_PATH" ]; then + echo "Activating Python virtual environment..." + source "$VENV_PATH/bin/activate" +else + echo "Warning: Python virtual environment not found at $VENV_PATH" + echo "Consider creating it with: python3 -m venv $VENV_PATH" +fi + +# Start Docker containers +echo "Starting Docker containers..." +cd "$SCRIPT_DIR" +docker compose up -d +sleep 5 + +# Setup local database +echo "Setting up local database..." +. "$SCRIPT_DIR/setup_local_db.sh" + +# Function to run test gogen commands +run_test_commands() { + echo "Waiting for SAM local to start..." + sleep 10 # Give SAM local some time to start + + echo "Running test gogen commands to validate API..." + GOGEN_APIURL=http://localhost:4000 gogen -c "$PROJECT_ROOT/examples/tutorial/tutorial1.yml" push tutorial1 + GOGEN_APIURL=http://localhost:4000 gogen -c coccyx/tutorial1 config + + echo "Test commands completed." +} + +# Build and start SAM application +echo "Building and starting SAM application..." +cd "$SCRIPT_DIR" +sam build + +# Start test commands in background +run_test_commands & +TEST_COMMANDS_PID=$! + +# Start SAM local in foreground +sam local start-api --host 0.0.0.0 --port 4000 --warm-containers EAGER --docker-network lambda-local + +# Trap Ctrl+C and call cleanup +cleanup() { + echo "Cleaning up..." + # Kill the test commands process if it's still running + if ps -p $TEST_COMMANDS_PID > /dev/null; then + kill $TEST_COMMANDS_PID + fi + cd "$SCRIPT_DIR" + docker compose down + exit 0 +} + +trap cleanup INT + +cleanup \ No newline at end of file diff --git a/gogen-api/stop_dev.sh b/gogen-api/stop_dev.sh new file mode 100755 index 0000000..e57b48c --- /dev/null +++ b/gogen-api/stop_dev.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cd "$(dirname "$0")" +docker compose down +docker ps | grep ecr | awk '{print $1}' | xargs docker kill +docker ps | grep dynamodb | awk '{print $1}' | xargs docker kill +docker ps | grep minio | awk '{print $1}' | xargs docker kill + diff --git a/gogen-api/table_schema.json b/gogen-api/table_schema.json new file mode 100644 index 0000000..e7f3b63 --- /dev/null +++ b/gogen-api/table_schema.json @@ -0,0 +1,19 @@ +{ + "TableName": "gogen", + "KeySchema": [ + { + "AttributeName": "gogen", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "gogen", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 1, + "WriteCapacityUnits": 1 + } +} \ No newline at end of file diff --git a/gogen-api/template.yaml b/gogen-api/template.yaml new file mode 100644 index 0000000..d9507f9 --- /dev/null +++ b/gogen-api/template.yaml @@ -0,0 +1,103 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + GoGenApi: + Type: AWS::Serverless::Api + Properties: + StageName: dev + Cors: + AllowMethods: "'GET,POST,OPTIONS'" + AllowHeaders: "'Content-Type,Authorization'" + AllowOrigin: "'*'" + + GetFunction: + Type: AWS::Serverless::Function + Metadata: + Dockerfile: Dockerfile + DockerContext: . + DockerTag: python3.13-v1 + Properties: + CodeUri: ./api + Handler: get.lambda_handler + Runtime: python3.13 + Events: + GetGogen: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/get/{proxy+} + Method: GET + + ListFunction: + Type: AWS::Serverless::Function + Metadata: + Dockerfile: Dockerfile + DockerContext: . + DockerTag: python3.13-v1 + Properties: + CodeUri: ./api + Handler: list.lambda_handler + Runtime: python3.13 + Events: + ListGogens: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/list + Method: GET + + SearchFunction: + Type: AWS::Serverless::Function + Metadata: + Dockerfile: Dockerfile + DockerContext: . + DockerTag: python3.13-v1 + Properties: + CodeUri: ./api + Handler: search.lambda_handler + Runtime: python3.13 + Events: + SearchGogens: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/search + Method: GET + + UpsertFunction: + Type: AWS::Serverless::Function + Metadata: + Dockerfile: Dockerfile + DockerContext: . + DockerTag: python3.13-v1 + Properties: + CodeUri: ./api + Handler: upsert.lambda_handler + Runtime: python3.13 + Events: + UpsertGogen: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/upsert + Method: POST + + DynamoDBTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: gogen + AttributeDefinitions: + - AttributeName: gogen + AttributeType: S + KeySchema: + - AttributeName: gogen + KeyType: HASH + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + +Outputs: + ApiURL: + Description: API Gateway endpoint URL + Value: !Sub "https://${GoGenApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" \ No newline at end of file diff --git a/gogen-api/test_dynamodb.py b/gogen-api/test_dynamodb.py new file mode 100644 index 0000000..91427a8 --- /dev/null +++ b/gogen-api/test_dynamodb.py @@ -0,0 +1,59 @@ +import boto3 +from botocore.config import Config +import logging +import sys + +# Set up logging to stdout +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logger.addHandler(handler) +# Remove any existing handlers to avoid duplicate logs +logger.propagate = False + +def test_connection(): + logger.info("Testing DynamoDB connection...") + + config = Config( + connect_timeout=10, + read_timeout=10, + retries={'max_attempts': 3} + ) + + try: + # Create DynamoDB client + dynamodb = boto3.resource('dynamodb', + endpoint_url='http://localhost:8000', + region_name='us-east-1', + aws_access_key_id='DUMMYIDEXAMPLE', + aws_secret_access_key='DUMMYEXAMPLEKEY', + config=config) + + # List tables + tables = list(dynamodb.tables.all()) + logger.info(f"Found tables: {[t.name for t in tables]}") + + # Test gogen table + table = dynamodb.Table('gogen') + + # Count items + scan_result = table.scan(Select='COUNT') + item_count = scan_result['Count'] + logger.info(f"Found {item_count} items in gogen table") + + # Get a sample of items + if item_count > 0: + sample = table.scan(Limit=3) + logger.info("Sample items:") + for item in sample['Items']: + logger.info(item) + + return True + + except Exception as e: + logger.error(f"Error testing DynamoDB: {str(e)}", exc_info=True) + return False + +if __name__ == '__main__': + test_connection() \ No newline at end of file diff --git a/gogen-api/test_s3.py b/gogen-api/test_s3.py new file mode 100644 index 0000000..f57cf33 --- /dev/null +++ b/gogen-api/test_s3.py @@ -0,0 +1,65 @@ +import boto3 +from botocore.config import Config +import logging +import sys + +# Set up logging to stdout +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logger.addHandler(handler) +# Remove any existing handlers to avoid duplicate logs +logger.propagate = False + +def test_s3_connection(): + """ + Test connection to local MinIO S3 server + """ + logger.info("Testing S3 connection...") + + config = Config( + connect_timeout=10, + read_timeout=10, + retries={'max_attempts': 3} + ) + + try: + # Create S3 client + s3 = boto3.resource('s3', + endpoint_url='http://localhost:9000', + region_name='us-east-1', + aws_access_key_id='minioadmin', + aws_secret_access_key='minioadmin', + config=config) + + # List buckets + buckets = list(s3.buckets.all()) + logger.info(f"Found buckets: {[b.name for b in buckets]}") + + # Test gogen-configs bucket + bucket = s3.Bucket('gogen-configs') + + # Upload a test file + test_content = "This is a test file for the gogen-configs bucket" + bucket.put_object(Key='test-config.txt', Body=test_content) + logger.info("Successfully uploaded test file to gogen-configs bucket") + + # List objects in bucket + logger.info("Objects in gogen-configs bucket:") + for obj in bucket.objects.all(): + logger.info(f"- {obj.key} (size: {obj.size} bytes)") + + # Download the test file + obj = bucket.Object('test-config.txt') + content = obj.get()['Body'].read().decode('utf-8') + logger.info(f"Downloaded content: {content}") + + return True + + except Exception as e: + logger.error(f"Error testing S3: {str(e)}", exc_info=True) + return False + +if __name__ == "__main__": + test_s3_connection() \ No newline at end of file diff --git a/gogen-api/upsert.py b/gogen-api/upsert.py deleted file mode 100644 index be41253..0000000 --- a/gogen-api/upsert.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import print_function - -import boto3 -import json -import httplib -from boto3.dynamodb.conditions import Key, Attr - -print('Loading function') - - -def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': err.message if err else json.dumps(res), - 'headers': { - 'Content-Type': 'application/json', - }, - } - - -def lambda_handler(event, context): - print("Received event: " + json.dumps(event, indent=2)) - body = json.loads(event['body']) - headers = { } - if 'Authorization' not in event['headers']: - return respond(Exception("Authorization header not present")) - headers['Authorization'] = event['headers']['Authorization'] - headers['User-Agent'] = 'gogen lambda' - headers['Content-Length'] = 0 - conn = httplib.HTTPSConnection('api.github.com') - conn.request("GET", "/user", None, headers) - response = conn.getresponse() - if response.status != 200: - return respond(Exception("Unable to authenticate user to GitHub, status: %d, msg: %s, data: %s" % (response.status, response.reason, response.read()))) - validatedbody = { } - for k, v in body.iteritems(): - if v != "": - validatedbody[k] = v - print("Item: ",validatedbody) - table = boto3.resource('dynamodb').Table('gogen') - response = table.put_item( - Item=validatedbody - ) - return respond(None, response) \ No newline at end of file diff --git a/internal/github.go b/internal/github.go index a43ca7f..6d210fb 100644 --- a/internal/github.go +++ b/internal/github.go @@ -3,15 +3,11 @@ package internal // Mostly from https://jacobmartins.com/2016/02/29/getting-started-with-oauth2-in-go/ import ( - "context" "io/ioutil" "net/http" "os" "path/filepath" - yaml "gopkg.in/yaml.v2" - - "github.com/kr/pretty" uuid "github.com/satori/go.uuid" "github.com/skratchdot/open-golang/open" @@ -42,77 +38,6 @@ type GitHub struct { client *github.Client } -// Push will create a public gist of "name.yml" from our running config -func (gh *GitHub) Push(name string, c *Config) *github.Gist { - gist := new(github.Gist) - files := make(map[github.GistFilename]github.GistFile) - - file := new(github.GistFile) - fname := name + ".yml" - file.Filename = &fname - var outb []byte - var outbs *string - var err error - if outb, err = yaml.Marshal(c); err != nil { - log.Fatalf("Cannot Marshal c.Global, err: %s", err) - } - outbs = new(string) - *outbs = string(outb) - file.Content = outbs - files[github.GistFilename(name)] = *file - - gist.Files = files - gist.Description = &name - public := true - gist.Public = &public - - var foundgist *github.Gist - foundgist = gh.findgist(name) - - if foundgist != nil { - log.Debugf("Found gist, updating") - updatedgist, _, err := gh.client.Gists.Edit(context.Background(), *foundgist.ID, gist) - if err != nil { - log.Fatalf("Error updating gist %# v: %s", pretty.Formatter(gist), err) - } - return updatedgist - } - log.Debugf("Gist not found, creating") - newgist, _, err := gh.client.Gists.Create(context.Background(), gist) - if err != nil { - log.Fatalf("Error creating gist %# v: %s", pretty.Formatter(gist), err) - } - return newgist -} - -// Pull grabs a named gist from GitHub -func (gh *GitHub) Pull(name string) *github.Gist { - var gist *github.Gist - gist = gh.findgist(name) - if gist == nil { - log.Fatalf("Error finding gist %s", name+".yml") - } - return gist -} - -func (gh *GitHub) findgist(name string) (foundgist *github.Gist) { - gists, _, err := gh.client.Gists.List(context.Background(), "", nil) - if err != nil { - log.Fatalf("Cannot pull Gists list: %s", err) - } -findgist: - for _, testgist := range gists { - for _, gistfile := range testgist.Files { - log.Debugf("Testing %s if match %s", *gistfile.Filename, name+".yml") - if *gistfile.Filename == name+".yml" { - foundgist = testgist - break findgist - } - } - } - return foundgist -} - // NewGitHub returns a GitHub object, with a set auth token func NewGitHub(requireauth bool) *GitHub { gh := new(GitHub) diff --git a/internal/gogen.go b/internal/gogen.go index 4b0b712..94029fb 100644 --- a/internal/gogen.go +++ b/internal/gogen.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/url" + "os" log "github.com/coccyx/gogen/logger" "github.com/kr/pretty" @@ -22,6 +23,7 @@ type GogenInfo struct { SampleEvent string `json:"sampleEvent"` GistID string `json:"gistID"` Version int `json:"version"` + Config string `json:"config"` } // GogenList is returned by the /v1/list and /v1/search APIs for Gogen @@ -32,13 +34,12 @@ type GogenList struct { // List calls /v1/list func List() []GogenList { - return listsearch("https://api.gogen.io/v1/list") - + return listsearch(fmt.Sprintf("%s/v1/list", getAPIURL())) } // Search calls /v1/search func Search(q string) []GogenList { - return listsearch("https://api.gogen.io/v1/search?q=" + url.QueryEscape(q)) + return listsearch(fmt.Sprintf("%s/v1/search?q=%s", getAPIURL(), url.QueryEscape(q))) } func listsearch(url string) (ret []GogenList) { @@ -77,9 +78,11 @@ func listsearch(url string) (ret []GogenList) { } // Get calls /v1/get -func Get(q string) (g GogenInfo, err error) { +var Get = func(q string) (g GogenInfo, err error) { client := &http.Client{} - resp, err := client.Get("https://api.gogen.io/v1/get/" + q) + url := fmt.Sprintf("%s/v1/get/%s", getAPIURL(), q) + log.Debugf("Calling %s", url) + resp, err := client.Get(url) if err != nil || resp.StatusCode != 200 { if resp != nil { if resp.StatusCode == 404 { @@ -103,29 +106,39 @@ func Get(q string) (g GogenInfo, err error) { if err != nil { return g, fmt.Errorf("Error unmarshaling body: %s", err) } + // log.Debugf("gogen: %# v", pretty.Formatter(gogen)) tmp, err := json.Marshal(gogen["Item"]) if err != nil { return g, fmt.Errorf("Error converting Item to JSON: %s", err) } + // log.Debugf("tmp: %s", string(tmp)) err = json.Unmarshal(tmp, &g) if err != nil { return g, fmt.Errorf("Error unmarshaling item: %s", err) } - log.Debugf("Gogen: %# v", pretty.Formatter(g)) + gCopy := g + gCopy.Config = "redacted" + log.Debugf("Gogen: %# v", pretty.Formatter(gCopy)) return g, nil } // Upsert calls /v1/upsert func Upsert(g GogenInfo) { gh := NewGitHub(true) + upsert(g, gh) +} + +func upsert(g GogenInfo, gh *GitHub) { client := &http.Client{} b, err := json.Marshal(g) if err != nil { log.Fatalf("Error marshaling Gogen %#v: %s", g, err) } + // log.Debugf("Body: %s", string(b)) - req, _ := http.NewRequest("POST", "https://api.gogen.io/v1/upsert", bytes.NewReader(b)) + req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v1/upsert", getAPIURL()), bytes.NewReader(b)) + // Still need GitHub token for authorization to verify user identity req.Header.Add("Authorization", "token "+gh.token) resp, err := client.Do(req) if err != nil || resp.StatusCode != 200 { @@ -136,5 +149,18 @@ func Upsert(g GogenInfo) { log.Fatalf("Error POSTing to upsert: %s", err) } } + // body, err := ioutil.ReadAll(resp.Body) + // if err != nil { + // log.Fatalf("Error reading response body: %s", err) + // } + // log.Debugf("Response Body: %s", body) log.Debugf("Upserted: %# v", pretty.Formatter(g)) } + +// getAPIURL returns the API URL from environment variable or default value +func getAPIURL() string { + if url := os.Getenv("GOGEN_APIURL"); url != "" { + return url + } + return "https://api.gogen.io" +} diff --git a/internal/gogen_test.go b/internal/gogen_test.go index 8910e3f..dbf287e 100644 --- a/internal/gogen_test.go +++ b/internal/gogen_test.go @@ -1,36 +1,348 @@ package internal -// func TestList(t *testing.T) { -// os.Setenv("GOGEN_FULLCONFIG", "") -// l := List() -// validateList(t, l) -// } - -// func TestSearch(t *testing.T) { -// l := Search("weblog") -// validateList(t, l) -// } - -// func TestGet(t *testing.T) { -// g, _ := Get("coccyx/weblog") -// assert.Equal(t, "coccyx/weblog", g.Gogen) -// assert.Equal(t, "weblog", g.Name) -// assert.Equal(t, "coccyx", g.Owner) -// } - -// func TestUpsert(t *testing.T) { -// g, _ := Get("coccyx/weblog") -// Upsert(g) -// } - -// func validateList(t *testing.T, l []GogenList) { -// if len(l) == 0 { -// t.Fatalf("Length of List() is 0") -// } -// if len(l[0].Gogen) == 0 { -// t.Fatalf("Gogen field of item 0 in List() is blank") -// } -// if len(l[0].Description) == 0 { -// t.Fatalf("Gogen description of item 0 in List() is blank") -// } -// } +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// mockGogenServer creates a test server that handles /v1/list, /v1/search, /v1/get, and /v1/upsert endpoints +func mockGogenServer(t *testing.T) (*httptest.Server, []GogenList) { + // Define the expected JSON response for list/search + mockResponse := `{"Items": [{"gogen": "coccyx/test3"}, {"description": "Generate CPU Metrics", "gogen": "coccyx/cpu"}, {"description": "Generate Disk Usage Metrics", "gogen": "coccyx/df"}, {"description": "Example of a custom lua generator", "gogen": "coccyx/users"}, {"gogen": "coccyx/test"}, {"description": "Simple CSV Example", "gogen": "coccyx/csv"}, {"description": "Weblog data in Apache common format", "gogen": "coccyx/weblog"}, {"description": "Generate CPU Metrics", "gogen": "coccyx/nixOS"}, {"description": "Example business event log from a middleware system, in key=value format.", "gogen": "coccyx/businessevent"}, {"description": "Generate Bandwidth Usage Metrics", "gogen": "coccyx/bandwidth"}, {"gogen": "coccyx/test2"}, {"description": "Generate Iostat Usage Metrics", "gogen": "coccyx/iostat"}, {"description": "Generate Memory Usage Metrics", "gogen": "coccyx/vmstat"}, {"description": "Tutorial 3", "gogen": "coccyx/tutorial3"}]}` + + // Define the expected JSON response for get + mockGetResponse := `{"Item": {"owner": "coccyx", "sampleEvent": "{\"_raw\":\"2017-01-31 14:38:00.440 transType=Change transID=1 transGUID=048a6346-6671-4703-89ec-3843afd44e85 userName=ivettaadelima city=\\\"HARTFORD\\\" state=CT zip=6101 value=1.206\",\"host\":\"server2.gogen.io\",\"index\":\"main\",\"source\":\"/var/log/translog\",\"sourcetype\":\"translog\"}\n", "gogen": "coccyx/tutorial3", "name": "tutorial3", "description": "Tutorial 3", "version": 3.0, "gistID": "0e3c9fda88915239b21d8a85a837750c", "config": "global:\n rotInterval: 1\n samplesDir:\n - .\nsamples:\n- name: tutorial3\n description: Tutorial 3\n disabled: false\n generator: sample\n rater: eventrater\n interval: 60\n count: 2\n earliest: now\n latest: now\n begin: 2012-02-09T08:00:00Z\n end: 2012-02-09T08:03:00Z\n tokens:\n - name: ts\n format: regex\n token: (\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2},\\d{3})\n type: gotimestamp\n replacement: 2006-01-02 15:04:05.000\n field: _raw\n - name: host\n format: template\n token: $host$\n type: choice\n field: host\n choice:\n - server1.gogen.io\n - server2.gogen.io\n - name: transtype\n format: regex\n token: transType=(\\w+)\n type: weightedChoice\n field: _raw\n weightedChoice:\n - weight: 3\n choice: New\n - weight: 5\n choice: Change\n - weight: 1\n choice: Delete\n - name: integerid\n format: template\n token: $integerid$\n type: script\n field: _raw\n script: |\n state[\"id\"] = state[\"id\"] + 1 return state[\"id\"]\n init:\n id: \"0\"\n - name: guid\n format: template\n token: $guid$\n type: random\n replacement: guid\n field: _raw\n - name: username\n format: template\n token: $username$\n type: choice\n sample: usernames.sample\n field: _raw\n choice:\n - birodivulga162\n - nildajcbonanno\n - ivettaadelima\n - pckomono\n - Looreeto\n - JooPedro1591\n - claaarecurlingg\n - acciokcavote\n - JungD\n - InaraAllves\n - Haroldmcaol\n - xNessaa\n - stylesdofunk\n - meltemmeltemm\n - emapujig\n - cellphones4deal\n - amisisuvi\n - MegSeecharran95\n - MargueritaYociu\n - MarcioBFasano\n - name: markets-city\n format: template\n token: $city$\n type: fieldChoice\n group: 1\n sample: markets.csv\n field: _raw\n srcField: city\n fieldChoice:\n - city: SPRINGFIELD\n county: HAMPDEN\n lat: \"42.106\"\n long: \"-72.5977\"\n state: MA\n zip: \"1101\"\n - city: WORCESTER\n county: WORCESTER\n lat: \"42.2621\"\n long: \"-71.8034\"\n state: MA\n zip: \"1601\"\n - city: WOBURN\n county: MIDDLESEX\n lat: \"42.482894\"\n long: \"-71.157404\"\n state: MA\n zip: \"1801\"\n - city: BOSTON\n county: SUFFOLK\n lat: \"42.345\"\n long: \"-71.0876\"\n state: MA\n zip: \"2123\"\n - city: MANCHESTER\n county: HILLSBOROUGH\n lat: \"42.992858\"\n long: \"-71.463255\"\n state: NH\n zip: \"3101\"\n - city: PORTLAND\n county: CUMBERLAND\n lat: \"43.660564\"\n long: \"-70.258864\"\n state: ME\n zip: \"4101\"\n - city: MONTPELIER\n county: WASHINGTON\n lat: \"44.2574\"\n long: \"-72.5698\"\n state: VT\n zip: \"5601\"\n - city: HARTFORD\n county: HARTFORD\n lat: \"41.7636\"\n long: \"-72.6855\"\n state: CT\n zip: \"6101\"\n - city: WEST HARTFORD\n county: HARTFORD\n lat: \"41.755553\"\n long: \"-72.75322\"\n state: CT\n zip: \"6107\"\n - name: markets-state\n format: template\n token: $state$\n type: fieldChoice\n group: 1\n sample: markets.csv\n field: _raw\n srcField: state\n fieldChoice:\n - city: SPRINGFIELD\n county: HAMPDEN\n lat: \"42.106\"\n long: \"-72.5977\"\n state: MA\n zip: \"1101\"\n - city: WORCESTER\n county: WORCESTER\n lat: \"42.2621\"\n long: \"-71.8034\"\n state: MA\n zip: \"1601\"\n - city: WOBURN\n county: MIDDLESEX\n lat: \"42.482894\"\n long: \"-71.157404\"\n state: MA\n zip: \"1801\"\n - city: BOSTON\n county: SUFFOLK\n lat: \"42.345\"\n long: \"-71.0876\"\n state: MA\n zip: \"2123\"\n - city: MANCHESTER\n county: HILLSBOROUGH\n lat: \"42.992858\"\n long: \"-71.463255\"\n state: NH\n zip: \"3101\"\n - city: PORTLAND\n county: CUMBERLAND\n lat: \"43.660564\"\n long: \"-70.258864\"\n state: ME\n zip: \"4101\"\n - city: MONTPELIER\n county: WASHINGTON\n lat: \"44.2574\"\n long: \"-72.5698\"\n state: VT\n zip: \"5601\"\n - city: HARTFORD\n county: HARTFORD\n lat: \"41.7636\"\n long: \"-72.6855\"\n state: CT\n zip: \"6101\"\n - city: WEST HARTFORD\n county: HARTFORD\n lat: \"41.755553\"\n long: \"-72.75322\"\n state: CT\n zip: \"6107\"\n - name: markets-zip\n format: template\n token: $zip$\n type: fieldChoice\n group: 1\n sample: markets.csv\n field: _raw\n srcField: zip\n fieldChoice:\n - city: SPRINGFIELD\n county: HAMPDEN\n lat: \"42.106\"\n long: \"-72.5977\"\n state: MA\n zip: \"1101\"\n - city: WORCESTER\n county: WORCESTER\n lat: \"42.2621\"\n long: \"-71.8034\"\n state: MA\n zip: \"1601\"\n - city: WOBURN\n county: MIDDLESEX\n lat: \"42.482894\"\n long: \"-71.157404\"\n state: MA\n zip: \"1801\"\n - city: BOSTON\n county: SUFFOLK\n lat: \"42.345\"\n long: \"-71.0876\"\n state: MA\n zip: \"2123\"\n - city: MANCHESTER\n county: HILLSBOROUGH\n lat: \"42.992858\"\n long: \"-71.463255\"\n state: NH\n zip: \"3101\"\n - city: PORTLAND\n county: CUMBERLAND\n lat: \"43.660564\"\n long: \"-70.258864\"\n state: ME\n zip: \"4101\"\n - city: MONTPELIER\n county: WASHINGTON\n lat: \"44.2574\"\n long: \"-72.5698\"\n state: VT\n zip: \"5601\"\n - city: HARTFORD\n county: HARTFORD\n lat: \"41.7636\"\n long: \"-72.6855\"\n state: CT\n zip: \"6101\"\n - city: WEST HARTFORD\n county: HARTFORD\n lat: \"41.755553\"\n long: \"-72.75322\"\n state: CT\n zip: \"6107\"\n - name: value\n format: regex\n token: value=(\\d+)\n type: random\n replacement: float\n field: _raw\n precision: 3\n upper: 10\n lines:\n - _raw: 2012-09-14 16:30:20,072 transType=ReplaceMe transID=$integerid$ transGUID=$guid$\n userName=$username$ city=\"$city$\" state=$state$ zip=$zip$ value=0\n host: $host$\n index: main\n source: /var/log/translog\n sourcetype: translog\n field: _raw\n singlepass: true\nmix: []\nraters:\n- name: eventrater\n type: config\n options:\n MinuteOfHour:\n 0: 1\n 1: 0.5\n 2: 2\n"}, "ResponseMetadata": {"RequestId": "a4e26206-1048-43a3-b5df-3e0d3f6989fa", "HTTPStatusCode": 200, "HTTPHeaders": {"server": "Jetty(12.0.14)", "date": "Thu, 06 Mar 2025 21:18:19 GMT", "x-amzn-requestid": "a4e26206-1048-43a3-b5df-3e0d3f6989fa", "content-type": "application/x-amz-json-1.0", "x-amz-crc32": "337133023", "content-length": "516"}, "RetryAttempts": 0}}` + + // Define the expected JSON response for upsert + mockUpsertResponse := `{"ResponseMetadata": {"RequestId": "99e49103-3b6e-4e8d-a01d-c8163e9f50c4", "HTTPStatusCode": 200, "HTTPHeaders": {"server": "Jetty(12.0.14)", "date": "Thu, 06 Mar 2025 21:27:11 GMT", "x-amzn-requestid": "99e49103-3b6e-4e8d-a01d-c8163e9f50c4", "content-type": "application/x-amz-json-1.0", "x-amz-crc32": "2745614147", "content-length": "2"}, "RetryAttempts": 0}}` + + // Parse the expected response to create our expected GogenList items + var expectedResponse map[string]interface{} + err := json.Unmarshal([]byte(mockResponse), &expectedResponse) + assert.NoError(t, err, "Failed to parse expected JSON response") + + // Extract the expected items that should be in the result + expectedItems := expectedResponse["Items"].([]interface{}) + + // Create a list of expected GogenList items that should be returned + // Only include items that have both gogen and description fields + var expectedList []GogenList + for _, item := range expectedItems { + itemMap := item.(map[string]interface{}) + gogen, hasGogen := itemMap["gogen"].(string) + description, hasDescription := itemMap["description"].(string) + + if hasGogen && hasDescription { + expectedList = append(expectedList, GogenList{ + Gogen: gogen, + Description: description, + }) + } + } + + // Create a mock server + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set the content type + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + + // Handle different endpoints + if r.URL.Path == "/v1/list" { + // Return the full mock response for list endpoint + w.Write([]byte(mockResponse)) + return + } else if r.URL.Path == "/v1/search" { + // Get the search query parameter + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Missing query parameter", http.StatusBadRequest) + return + } + + // Filter the items based on the query + var filteredItems []map[string]interface{} + for _, item := range expectedItems { + itemMap := item.(map[string]interface{}) + gogen, hasGogen := itemMap["gogen"].(string) + + // Include the item if its gogen field contains the query string + if hasGogen && strings.Contains(strings.ToLower(gogen), strings.ToLower(query)) { + filteredItems = append(filteredItems, itemMap) + } + } + + // Create a filtered response + filteredResponse := map[string]interface{}{ + "Items": filteredItems, + } + + // Convert to JSON and return + filteredJSON, err := json.Marshal(filteredResponse) + if err != nil { + http.Error(w, "Error creating response", http.StatusInternalServerError) + return + } + + w.Write(filteredJSON) + return + } else if strings.HasPrefix(r.URL.Path, "/v1/get/") { + // Extract the gogen identifier from the path + gogenID := strings.TrimPrefix(r.URL.Path, "/v1/get/") + + // Check if the requested gogen is the one we have a mock response for + if gogenID == "coccyx/tutorial3" { + w.Write([]byte(mockGetResponse)) + return + } else { + // Return a 404 for any other gogen ID + http.Error(w, "Gogen not found", http.StatusNotFound) + return + } + } else if r.URL.Path == "/v1/upsert" { + // Check if the request method is POST + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Check if the request has an Authorization header + authHeader := r.Header.Get("Authorization") + if !strings.HasPrefix(authHeader, "token ") { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Read the request body + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error reading request body", http.StatusBadRequest) + return + } + + // Parse the request body to verify it's valid JSON + var gogen GogenInfo + err = json.Unmarshal(body, &gogen) + if err != nil { + http.Error(w, "Invalid JSON in request body", http.StatusBadRequest) + return + } + + // Verify that the required fields are present + if gogen.Gogen == "" || gogen.Owner == "" || gogen.Name == "" { + http.Error(w, "Missing required fields", http.StatusBadRequest) + return + } + + // Return the mock upsert response + w.Write([]byte(mockUpsertResponse)) + return + } else { + // Handle unknown paths + http.Error(w, "Not found", http.StatusNotFound) + return + } + })) + + return mockServer, expectedList +} + +func TestListWithMockServer(t *testing.T) { + // Create the mock server + mockServer, expectedList := mockGogenServer(t) + defer mockServer.Close() + + // Set the GOGEN_APIURL environment variable to point to our mock server + originalAPIURL := os.Getenv("GOGEN_APIURL") + os.Setenv("GOGEN_APIURL", mockServer.URL) + defer os.Setenv("GOGEN_APIURL", originalAPIURL) // Restore the original value when done + + // Call the List function + result := List() + + // Verify the results + assert.NotEmpty(t, result, "List result should not be empty") + + // Check that we have the correct number of items + assert.Equal(t, len(expectedList), len(result), "Number of items should match") + + // Create maps for easier comparison + resultMap := make(map[string]string) + for _, item := range result { + resultMap[item.Gogen] = item.Description + } + + expectedMap := make(map[string]string) + for _, item := range expectedList { + expectedMap[item.Gogen] = item.Description + } + + // Verify that the maps are identical + assert.Equal(t, expectedMap, resultMap, "The result should exactly match the expected items") + + // Verify each individual item for more detailed error messages if there's a mismatch + for _, expected := range expectedList { + found := false + for _, actual := range result { + if expected.Gogen == actual.Gogen { + found = true + assert.Equal(t, expected.Description, actual.Description, + "Description mismatch for %s. Expected: %s, Got: %s", + expected.Gogen, expected.Description, actual.Description) + break + } + } + assert.True(t, found, "Expected item %s not found in result", expected.Gogen) + } +} + +func TestSearchWithMockServer(t *testing.T) { + // Create the mock server + mockServer, allExpectedItems := mockGogenServer(t) + defer mockServer.Close() + + // Set the GOGEN_APIURL environment variable to point to our mock server + originalAPIURL := os.Getenv("GOGEN_APIURL") + os.Setenv("GOGEN_APIURL", mockServer.URL) + defer os.Setenv("GOGEN_APIURL", originalAPIURL) // Restore the original value when done + + // Define search queries and expected results + testCases := []struct { + query string + expected []GogenList + }{ + { + query: "cpu", + expected: filterExpectedItems(allExpectedItems, "cpu"), + }, + { + query: "weblog", + expected: filterExpectedItems(allExpectedItems, "weblog"), + }, + { + query: "coccyx", + expected: allExpectedItems, // All items contain "coccyx" + }, + } + + for _, tc := range testCases { + t.Run("Search_"+tc.query, func(t *testing.T) { + // Call the Search function + result := Search(tc.query) + + // Verify the results + assert.NotEmpty(t, result, "Search result should not be empty for query: %s", tc.query) + + // Check that we have the correct number of items + assert.Equal(t, len(tc.expected), len(result), + "Number of items should match for query: %s. Expected: %d, Got: %d", + tc.query, len(tc.expected), len(result)) + + // Create maps for easier comparison + resultMap := make(map[string]string) + for _, item := range result { + resultMap[item.Gogen] = item.Description + } + + expectedMap := make(map[string]string) + for _, item := range tc.expected { + expectedMap[item.Gogen] = item.Description + } + + // Verify that the maps are identical + assert.Equal(t, expectedMap, resultMap, + "The result should exactly match the expected items for query: %s", tc.query) + }) + } +} + +func TestGetWithMockServer(t *testing.T) { + // Create the mock server + mockServer, _ := mockGogenServer(t) + defer mockServer.Close() + + // Set the GOGEN_APIURL environment variable to point to our mock server + originalAPIURL := os.Getenv("GOGEN_APIURL") + os.Setenv("GOGEN_APIURL", mockServer.URL) + defer os.Setenv("GOGEN_APIURL", originalAPIURL) // Restore the original value when done + + // Call the Get function with the ID that should return a valid response + result, err := Get("coccyx/tutorial3") + + // Verify there was no error + assert.NoError(t, err, "Get should not return an error for a valid Gogen ID") + + // Verify the result fields match what we expect + assert.Equal(t, "coccyx/tutorial3", result.Gogen, "Gogen field should match") + assert.Equal(t, "coccyx", result.Owner, "Owner field should match") + assert.Equal(t, "tutorial3", result.Name, "Name field should match") + assert.Equal(t, "Tutorial 3", result.Description, "Description field should match") + assert.Equal(t, "0e3c9fda88915239b21d8a85a837750c", result.GistID, "GistID field should match") + assert.Equal(t, 3, result.Version, "Version field should match") + assert.True(t, len(result.Config) > 0, "Config field should not be empty") + assert.True(t, len(result.SampleEvent) > 0, "SampleEvent field should not be empty") + + // Test with an invalid Gogen ID + _, err = Get("coccyx/nonexistent") + assert.Error(t, err, "Get should return an error for an invalid Gogen ID") + assert.Contains(t, err.Error(), "Could not find Gogen", "Error message should indicate Gogen not found") +} + +// Mock the GitHub struct for testing Upsert +type mockGitHub struct { + token string +} + +// Store the original NewGitHub function +var originalNewGitHub func(bool) *GitHub + +func init() { + // Save the original function + originalNewGitHub = NewGitHub +} + +func TestUpsertWithMockServer(t *testing.T) { + // Create the mock server + mockServer, _ := mockGogenServer(t) + defer mockServer.Close() + + // Set the GOGEN_APIURL environment variable to point to our mock server + originalAPIURL := os.Getenv("GOGEN_APIURL") + os.Setenv("GOGEN_APIURL", mockServer.URL) + defer os.Setenv("GOGEN_APIURL", originalAPIURL) // Restore the original value when done + + // Create a GogenInfo object to upsert + gogen := GogenInfo{ + Gogen: "coccyx/tutorial1", + Owner: "coccyx", + Name: "tutorial1", + Description: "Tutorial 1", + Notes: "", + SampleEvent: "{\"_raw\":\"Mar/06/25 13:24:28 line3\"}\n", + GistID: "9edab9605421c036ce2ebef5b4966b1d", + Version: 1, + Config: "", + } + + // Call the Upsert function + upsert(gogen, &GitHub{token: "mock-github-token"}) + + // Since Upsert doesn't return anything, we can only verify that it didn't panic + // The mock server will return an error if the request is not formatted correctly +} + +// Helper function to filter expected items based on a query string +func filterExpectedItems(items []GogenList, query string) []GogenList { + var filtered []GogenList + for _, item := range items { + if strings.Contains(strings.ToLower(item.Gogen), strings.ToLower(query)) { + filtered = append(filtered, item) + } + } + return filtered +} diff --git a/internal/share.go b/internal/share.go index dd50363..08e72f1 100644 --- a/internal/share.go +++ b/internal/share.go @@ -1,22 +1,19 @@ package internal import ( - "bytes" "context" "encoding/csv" "fmt" - "io" "io/ioutil" - "net/http" "net/url" "os" "path/filepath" "sort" "strconv" "strings" + "time" log "github.com/coccyx/gogen/logger" - "github.com/google/go-github/github" yaml "gopkg.in/yaml.v2" ) @@ -25,8 +22,8 @@ type Run interface { Once(sample string) } -// Push pushes the running config to the Gogen API and creates a GitHub gist. Returns the owner and ID of the Gist. -func Push(name string, run Run) (string, string) { +// Push pushes the running config to the Gogen API. Returns the owner and empty string (for backward compatibility). +func Push(name string, run Run) string { c := NewConfig() ec := BuildConfig(ConfigConfig{ FullConfig: c.cc.FullConfig, @@ -46,20 +43,25 @@ func Push(name string, run Run) (string, string) { FullConfig: m.Sample, Export: true, }) - login, _ := push(sc.Samples[0].Name, sc, sc, run) + login := push(sc.Samples[0].Name, sc, sc, run) ec.Mix[i].Sample = login + "/" + sc.Samples[0].Name } } return push(name, c, ec, run) } log.Panicf("No samples configured") - return "", "" + return "" } -func push(name string, genc *Config, pushc *Config, run Run) (string, string) { +func push(name string, genc *Config, pushc *Config, run Run) string { log.Debugf("Pushing config as '%s'", name) gh := NewGitHub(true) - user, _, err := gh.client.Users.Get(context.Background(), "") + + // Create a context with a 5-second timeout for GitHub user retrieval + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + user, _, err := gh.client.Users.Get(ctx, "") if err != nil { log.Fatalf("Error getting user in push: %s", err) } @@ -82,7 +84,12 @@ func push(name string, genc *Config, pushc *Config, run Run) (string, string) { } else { version = oldGogen.Version + 1 } - gi := gh.Push(name, pushc) + + // Convert config to YAML string + configYaml, err := yaml.Marshal(pushc) + if err != nil { + log.Fatalf("Error marshaling config to YAML: %s", err) + } g := GogenInfo{ Gogen: gogen, @@ -91,17 +98,17 @@ func push(name string, genc *Config, pushc *Config, run Run) (string, string) { Notes: sample.Notes, Owner: *user.Login, SampleEvent: genc.Buf.String(), - GistID: *gi.ID, Version: version, + Config: string(configYaml), } Upsert(g) - return *user.Login, *gi.ID + return *user.Login } - return "", "" + return "" } -// Pull grabs a config from the Gogen API + GitHub gist and creates it on the filesystem for editing +// Pull grabs a config from the Gogen API and creates it on the filesystem for editing func Pull(gogen string, dir string, deconstruct bool) { gogentokens := strings.Split(gogen, "/") var name string @@ -114,155 +121,40 @@ func Pull(gogen string, dir string, deconstruct bool) { if err != nil { log.WithError(err).Fatalf("error retrieving gogen config for gogen '%s'", gogen) } - gist := getGist(g) - for _, file := range gist.Files { - filename := filepath.Join(dir, *file.Filename) - client := &http.Client{} - resp, err := client.Get(*file.RawURL) - f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - log.Fatalf("Couldn't open file %s: %s", filename, err) - } - _, err = io.Copy(f, resp.Body) - if err != nil { - log.Fatalf("Error writing to file %s: %s", filename, err) - } - f.Close() - if deconstruct { - samplesDir := filepath.Join(dir, "samples") - templatesDir := filepath.Join(dir, "templates") - generatorsDir := filepath.Join(dir, "generators") - err := os.Mkdir(samplesDir, 0755) - err = os.Mkdir(templatesDir, 0755) - err = os.Mkdir(generatorsDir, 0755) - if err != nil && !os.IsExist(err) { - log.Fatalf("Error creating directories %s or %s", samplesDir, templatesDir) - } - - cc := ConfigConfig{FullConfig: filename, Export: true} - c := BuildConfig(cc) - for x := 0; x < len(c.Samples); x++ { - s := c.Samples[x] - for y := 0; y < len(s.Tokens); y++ { - t := c.Samples[x].Tokens[y] - if t.SampleString != "" { - fname := t.SampleString - if fname[len(fname)-6:] == "sample" { - f, err := os.OpenFile(filepath.Join(samplesDir, fname), os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - log.Fatalf("Unable to open file %s: %s", filepath.Join(samplesDir, fname), err) - } - defer f.Close() - for _, v := range t.Choice { - _, err := fmt.Fprintf(f, "%s\n", v) - if err != nil { - log.Fatalf("Error writing to file %s: %s", filepath.Join(samplesDir, fname), err) - } - } - c.Samples[x].Tokens[y].Choice = []string{} - } else if fname[len(fname)-3:] == "csv" { - if len(s.Lines) > 0 { - f, err := os.OpenFile(filepath.Join(samplesDir, fname), os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - log.Fatalf("Unable to open file %s: %s", filepath.Join(samplesDir, fname), err) - } - defer f.Close() - w := csv.NewWriter(f) - - keys := make([]string, len(t.FieldChoice[0])) - i := 0 - for k := range t.FieldChoice[0] { - keys[i] = k - i++ - } - sort.Strings(keys) - w.Write(keys) - - for _, l := range t.FieldChoice { - values := make([]string, len(keys)) - for j, k := range keys { - values[j] = l[k] - } - w.Write(values) - } - - w.Flush() - c.Samples[x].Tokens[y].FieldChoice = []map[string]string{} - } - } - - var outb []byte - var err error - if outb, err = yaml.Marshal(s); err != nil { - log.Fatalf("Cannot Marshal sample '%s', err: %s", s.Name, err) - } - outfname := filepath.Join(samplesDir, name+".yml") - log.Debugf("Writing sample file for sammple '%s' at file: %s", s.Name, outfname) - err = ioutil.WriteFile(outfname, outb, 0644) - if err != nil { - log.Fatalf("Cannot write file %s: %s", outfname, err) - } - } - } - } - - for _, t := range c.Templates { - var outb []byte - var err error - if outb, err = yaml.Marshal(t); err != nil { - log.Fatalf("Cannot Marshal template '%s', err: %s", t.Name, err) - } - err = ioutil.WriteFile(filepath.Join(templatesDir, t.Name+".yml"), outb, 0644) - if err != nil { - log.Fatalf("Error writing file %s", filepath.Join(templatesDir, t.Name+".yml")) - } - } - for i, g := range c.Generators { - if g.FileName != "" { - fname := filepath.Base(g.FileName) - err = ioutil.WriteFile(filepath.Join(generatorsDir, fname), []byte(g.Script), 0644) - if err != nil { - log.Fatalf("Error writing file %s", filepath.Join(generatorsDir, fname)) - } - c.Generators[i].FileName = fname - c.Generators[i].Script = "" - } - - var outb []byte - var err error - if outb, err = yaml.Marshal(g); err != nil { - log.Fatalf("Cannot Marshal generator '%s', err: %s", g.Name, err) - } - err = ioutil.WriteFile(filepath.Join(generatorsDir, g.Name+".yml"), outb, 0644) - if err != nil { - log.Fatalf("Error writing file %s", filepath.Join(generatorsDir, g.Name+".yml")) - } - } + // Check if we have the config content + if g.Config == "" { + log.Fatalf("No configuration content found for gogen '%s'", gogen) + } - for _, g := range c.Mix { - Pull(g.Sample, dir, true) - } + // Write the config to a file + filename := filepath.Join(dir, name+".yml") + err = ioutil.WriteFile(filename, []byte(g.Config), 0644) + if err != nil { + log.Fatalf("Error writing to file %s: %s", filename, err) + } - err = os.Remove(filename) - if err != nil { - log.Debugf("Error removing original config file during deconstruction: %s", filename) - } - } - break + if deconstruct { + deconstructConfig(filename, name, dir) } } -// PullFile pulls a config from the Gogen API + GitHub gist and writes it to a single file +// PullFile pulls a config from the Gogen API and writes it to a single file func PullFile(gogen string, filename string) { g, err := Get(gogen) if err != nil { log.WithError(err).Fatalf("error retrieving gogen config for gogen '%s'", gogen) } + + // Check if we have the config content + if g.Config == "" { + log.Fatalf("No configuration content found for gogen '%s'", gogen) + } + + var configContent []byte var version int cached := false - var readFrom io.ReadCloser cacheFile := filepath.Join(os.ExpandEnv("$GOGEN_TMPDIR"), ".configcache_"+url.QueryEscape(gogen)) versionCacheFile := filepath.Join(os.ExpandEnv("$GOGEN_TMPDIR"), ".versioncache_"+url.QueryEscape(gogen)) _, err = os.Stat(versionCacheFile) @@ -277,7 +169,7 @@ func PullFile(gogen string, filename string) { } if version == g.Version { log.Debugf("Reading config from cache file '%s'", cacheFile) - readFrom, err = os.Open(cacheFile) + configContent, err = ioutil.ReadFile(cacheFile) if err != nil { cached = false } else { @@ -287,34 +179,19 @@ func PullFile(gogen string, filename string) { log.Debugf("Version mismatch, Gogen version %d cached version %d", g.Version, version) } } + if !cached { - gist := getGist(g) - for _, file := range gist.Files { - log.Debugf("Reading config from GitHub") - client := &http.Client{} - resp, err := client.Get(*file.RawURL) - if err != nil { - log.Fatalf("Could not read from HTTP url '%s' for gist '%s': %s", *file.RawURL, gogen, err) - } - readFrom = resp.Body - break - } - } - // Make a copy of readFrom in case we need to write it to cache - var buf bytes.Buffer - if _, err = buf.ReadFrom(readFrom); err != nil { - log.Fatalf("Couldn't read from readFrom into buffer: %s", err) - } - if err = readFrom.Close(); err != nil { - log.Fatalf("Error closing readFrom: %s", err) + log.Debugf("Using config content from API response") + configContent = []byte(g.Config) } + // Write the config to the specified file f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644) defer f.Close() if err != nil { log.Fatalf("Couldn't open file %s: %s", filename, err) } - _, err = io.Copy(f, bytes.NewReader(buf.Bytes())) + _, err = f.Write(configContent) if err != nil { log.Fatalf("Error writing to file %s: %s", filename, err) } @@ -336,18 +213,132 @@ func PullFile(gogen string, filename string) { if err != nil { log.Fatalf("Couldn't open cache file '%s': %s", cacheFile, err) } - _, err = io.Copy(cachef, bytes.NewReader(buf.Bytes())) + _, err = cachef.Write(configContent) if err != nil { log.Fatalf("Error writing to cache file '%s': %s", cacheFile, err) } } } -func getGist(g GogenInfo) (gist *github.Gist) { - gh := NewGitHub(false) - gist, _, err := gh.client.Gists.Get(context.Background(), g.GistID) +// Helper function to deconstruct a config file +func deconstructConfig(filename string, name string, dir string) { + samplesDir := filepath.Join(dir, "samples") + templatesDir := filepath.Join(dir, "templates") + generatorsDir := filepath.Join(dir, "generators") + err := os.Mkdir(samplesDir, 0755) + err = os.Mkdir(templatesDir, 0755) + err = os.Mkdir(generatorsDir, 0755) + if err != nil && !os.IsExist(err) { + log.Fatalf("Error creating directories %s or %s", samplesDir, templatesDir) + } + + cc := ConfigConfig{FullConfig: filename, Export: true} + c := BuildConfig(cc) + for x := 0; x < len(c.Samples); x++ { + s := c.Samples[x] + for y := 0; y < len(s.Tokens); y++ { + t := c.Samples[x].Tokens[y] + if t.SampleString != "" { + fname := t.SampleString + if fname[len(fname)-6:] == "sample" { + f, err := os.OpenFile(filepath.Join(samplesDir, fname), os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + log.Fatalf("Unable to open file %s: %s", filepath.Join(samplesDir, fname), err) + } + defer f.Close() + for _, v := range t.Choice { + _, err := fmt.Fprintf(f, "%s\n", v) + if err != nil { + log.Fatalf("Error writing to file %s: %s", filepath.Join(samplesDir, fname), err) + } + } + c.Samples[x].Tokens[y].Choice = []string{} + } else if fname[len(fname)-3:] == "csv" { + if len(s.Lines) > 0 { + f, err := os.OpenFile(filepath.Join(samplesDir, fname), os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + log.Fatalf("Unable to open file %s: %s", filepath.Join(samplesDir, fname), err) + } + defer f.Close() + w := csv.NewWriter(f) + + keys := make([]string, len(t.FieldChoice[0])) + i := 0 + for k := range t.FieldChoice[0] { + keys[i] = k + i++ + } + sort.Strings(keys) + w.Write(keys) + + for _, l := range t.FieldChoice { + values := make([]string, len(keys)) + for j, k := range keys { + values[j] = l[k] + } + w.Write(values) + } + + w.Flush() + c.Samples[x].Tokens[y].FieldChoice = []map[string]string{} + } + } + + var outb []byte + var err error + if outb, err = yaml.Marshal(s); err != nil { + log.Fatalf("Cannot Marshal sample '%s', err: %s", s.Name, err) + } + outfname := filepath.Join(samplesDir, name+".yml") + log.Debugf("Writing sample file for sammple '%s' at file: %s", s.Name, outfname) + err = ioutil.WriteFile(outfname, outb, 0644) + if err != nil { + log.Fatalf("Cannot write file %s: %s", outfname, err) + } + } + } + } + + for _, t := range c.Templates { + var outb []byte + var err error + if outb, err = yaml.Marshal(t); err != nil { + log.Fatalf("Cannot Marshal template '%s', err: %s", t.Name, err) + } + err = ioutil.WriteFile(filepath.Join(templatesDir, t.Name+".yml"), outb, 0644) + if err != nil { + log.Fatalf("Error writing file %s", filepath.Join(templatesDir, t.Name+".yml")) + } + } + + for i, g := range c.Generators { + if g.FileName != "" { + fname := filepath.Base(g.FileName) + err = ioutil.WriteFile(filepath.Join(generatorsDir, fname), []byte(g.Script), 0644) + if err != nil { + log.Fatalf("Error writing file %s", filepath.Join(generatorsDir, fname)) + } + c.Generators[i].FileName = fname + c.Generators[i].Script = "" + } + + var outb []byte + var err error + if outb, err = yaml.Marshal(g); err != nil { + log.Fatalf("Cannot Marshal generator '%s', err: %s", g.Name, err) + } + err = ioutil.WriteFile(filepath.Join(generatorsDir, g.Name+".yml"), outb, 0644) + if err != nil { + log.Fatalf("Error writing file %s", filepath.Join(generatorsDir, g.Name+".yml")) + } + } + + for _, g := range c.Mix { + Pull(g.Sample, dir, true) + } + + err = os.Remove(filename) if err != nil { - log.Fatalf("Couldn't get gist: %s", err) + log.Debugf("Error removing original config file during deconstruction: %s", filename) } - return gist } diff --git a/internal/share_test.go b/internal/share_test.go index ec4dcbe..1793336 100644 --- a/internal/share_test.go +++ b/internal/share_test.go @@ -1,35 +1,148 @@ package internal -// func TestSharePull(t *testing.T) { -// os.Setenv("GOGEN_HOME", "..") -// _ = os.Mkdir("testout", 0777) -// Pull("coccyx/weblog", "testout", false) -// _, err := os.Stat("testout/weblog.yml") -// assert.NoError(t, err, "Couldn't find file weblog.yml") -// _ = os.Remove("testout/weblog.yml") - -// Pull("coccyx/weblog", "testout", true) -// _, err = os.Stat("testout/samples/weblog.yml") -// assert.NoError(t, err, "Couldn't find file samples/weblog.yml") -// _, err = os.Stat("testout/samples/webhosts.csv") -// assert.NoError(t, err, "Couldn't find file samples/webhosts.csv") -// _, err = os.Stat("testout/samples/useragents.sample") -// assert.NoError(t, err, "Couldn't find file samples/useragents.sample") -// _ = os.RemoveAll("testout") - -// } - -// func TestSharePullFile(t *testing.T) { -// os.Setenv("GOGEN_TMPDIR", "..") -// os.Setenv("GOGEN_HOME", "..") -// os.Remove("../.versioncachefile_coccyx%2Fweblog") -// os.Remove("../.configcache_coccyx%2Fweblog") -// PullFile("coccyx/weblog", ".test.json") -// _, err := os.Stat(".test.json") -// assert.NoError(t, err, "Couldn't fine .test.json") -// _, err = os.Stat(filepath.Join(os.ExpandEnv("$GOGEN_TMPDIR"), ".versioncache_coccyx%2Fweblog")) -// assert.NoError(t, err, "Couldn't fine version cache file") -// _, err = os.Stat(filepath.Join(os.ExpandEnv("$GOGEN_TMPDIR"), ".configcache_coccyx%2Fweblog")) -// assert.NoError(t, err, "Couldn't find cache file") -// os.Remove(".test.json") -// } +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSharePull(t *testing.T) { + // Mock the Get function to return a GogenInfo with Config + originalGet := Get + defer func() { Get = originalGet }() + + Get = func(q string) (GogenInfo, error) { + return GogenInfo{ + Gogen: "testuser/testconfig", + Name: "testconfig", + Description: "Test configuration", + Notes: "Test notes", + Owner: "testuser", + Version: 1, + Config: "sample: test\nname: testconfig", + }, nil + } + + os.Setenv("GOGEN_HOME", "..") + _ = os.Mkdir("testout", 0777) + defer os.RemoveAll("testout") + + Pull("testuser/testconfig", "testout", false) + _, err := os.Stat("testout/testconfig.yml") + assert.NoError(t, err, "Couldn't find file testconfig.yml") +} + +func TestSharePullWithDeconstruct(t *testing.T) { + // Mock the Get function to return a GogenInfo with Config + originalGet := Get + defer func() { Get = originalGet }() + + configYaml := ` +global: + debug: false + verbose: false + generatorWorkers: 1 + outputWorkers: 1 + rotInterval: 1 + output: + outputter: file + fileName: /tmp/testconfig.log + maxBytes: 102400 + outputTemplate: json +samples: + - name: testconfig + description: Test configuration + notes: Test notes + endIntervals: 100 + interval: 1 + count: 100 + tokens: + - name: host + format: template + type: choice + field: host + sample: hosts.sample + choice: + - host1 + - host2 + - name: useragent + format: template + type: choice + field: useragent + choice: + - "Mozilla/5.0" + - "Chrome/51.0" + sample: useragents.sample + - name: value + format: template + type: random + replacement: float + precision: 3 + lower: 0 + upper: 10 + lines: + - _raw: host=$host$ useragent="$useragent$" value=$value$ +` + + Get = func(q string) (GogenInfo, error) { + return GogenInfo{ + Gogen: "testuser/testconfig", + Name: "testconfig", + Description: "Test configuration", + Notes: "Test notes", + Owner: "testuser", + Version: 1, + Config: configYaml, + }, nil + } + + os.Setenv("GOGEN_HOME", "..") + _ = os.Mkdir("testout", 0777) + defer os.RemoveAll("testout") + + Pull("testuser/testconfig", "testout", true) + _, err := os.Stat("testout/samples/testconfig.yml") + assert.NoError(t, err, "Couldn't find file samples/testconfig.yml") + _, err = os.Stat("testout/samples/hosts.sample") + assert.NoError(t, err, "Couldn't find file samples/hosts.sample") + _, err = os.Stat("testout/samples/useragents.sample") + assert.NoError(t, err, "Couldn't find file samples/useragents.sample") +} + +func TestSharePullFile(t *testing.T) { + // Mock the Get function to return a GogenInfo with Config + originalGet := Get + defer func() { Get = originalGet }() + + Get = func(q string) (GogenInfo, error) { + return GogenInfo{ + Gogen: "testuser/testconfig", + Name: "testconfig", + Description: "Test configuration", + Notes: "Test notes", + Owner: "testuser", + Version: 1, + Config: "sample: test\nname: testconfig", + }, nil + } + + os.Setenv("GOGEN_TMPDIR", "..") + os.Setenv("GOGEN_HOME", "..") + os.Remove("../.versioncache_testuser%2Ftestconfig") + os.Remove("../.configcache_testuser%2Ftestconfig") + defer func() { + os.Remove(".test.yml") + os.Remove("../.versioncache_testuser%2Ftestconfig") + os.Remove("../.configcache_testuser%2Ftestconfig") + }() + + PullFile("testuser/testconfig", ".test.yml") + _, err := os.Stat(".test.yml") + assert.NoError(t, err, "Couldn't find .test.yml") + _, err = os.Stat(filepath.Join(os.ExpandEnv("$GOGEN_TMPDIR"), ".versioncache_testuser%2Ftestconfig")) + assert.NoError(t, err, "Couldn't find version cache file") + _, err = os.Stat(filepath.Join(os.ExpandEnv("$GOGEN_TMPDIR"), ".configcache_testuser%2Ftestconfig")) + assert.NoError(t, err, "Couldn't find cache file") +} diff --git a/main.go b/main.go index 76824fb..7b9690d 100644 --- a/main.go +++ b/main.go @@ -408,7 +408,6 @@ func main() { fmt.Printf("%15s : %s\n", "Owner", g.Owner) fmt.Printf("%15s : %s\n", "Name", g.Name) fmt.Printf("%15s : %s\n", "Description", g.Description) - fmt.Printf("%15s : %s\n", "Gist Link", fmt.Sprintf("https://gist.github.com/%s/%s", g.Owner, g.GistID)) if len(g.Notes) > 0 { fmt.Printf("Notes:\n") fmt.Printf("------------------------------------------------------\n") @@ -432,9 +431,9 @@ func main() { Name: "push", Usage: "Push running config to Gogen sharing service", ArgsUsage: "[name]\n\n" + "This will push your running config to the Gogen sharing API. This will publish the running config\n" + - "in a Git Gist and make an entry in the Gogen API database pointing to the gist with a bit of metadata.\n\n" + - "The [name] argument will be the name of the config published. The entry in the database\n" + - "will get its Description and Notes from the first sample. If a mix is specified, it will\n" + + "to the Gogen API.\n\n" + + "The [name] argument will be the name of the config published. The owner will be your GitHub ID.\n" + + "The entry in the database will get its Description and Notes from the first sample. If a mix is specified, it will\n" + "attempt to push all referenced configs in the sample.", Action: func(clic *cli.Context) error { // config.ResetConfig() @@ -444,8 +443,9 @@ func main() { os.Exit(1) } var r run.Runner - owner, id := config.Push(clic.Args().First(), r) - fmt.Printf("Push successful. Gist: https://gist.github.com/%s/%s\n", owner, id) + name := clic.Args().First() + owner := config.Push(name, r) + fmt.Printf("Push successful. Config: %s/%s\n", owner, name) return nil }, }, diff --git a/setup_venv.sh b/setup_venv.sh new file mode 100755 index 0000000..6380c57 --- /dev/null +++ b/setup_venv.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Script to set up the Python virtual environment for the Gogen API project + +# Define paths +VENV_PATH=".pyvenv" +PROJECT_ROOT="$(pwd)" +API_DIR="$PROJECT_ROOT/gogen-api" + +# Check if virtual environment already exists +if [ -d "$VENV_PATH" ]; then + echo "Virtual environment already exists at $VENV_PATH" + echo "To recreate it, delete the directory first: rm -rf $VENV_PATH" + echo "To activate it: source $VENV_PATH/bin/activate" + exit 0 +fi + +# Check Python version +PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}') +echo "Using Python version: $PYTHON_VERSION" + +# Create virtual environment +echo "Creating virtual environment at $VENV_PATH..." +python3 -m venv "$VENV_PATH" + +if [ ! -d "$VENV_PATH" ]; then + echo "Failed to create virtual environment. Please check your Python installation." + exit 1 +fi + +# Activate virtual environment +echo "Activating virtual environment..." +source "$VENV_PATH/bin/activate" + +# Upgrade pip +echo "Upgrading pip..." +pip install --upgrade pip + +# Install requirements if they exist +if [ -f "$API_DIR/requirements.txt" ]; then + echo "Installing requirements from $API_DIR/requirements.txt..." + pip install -r "$API_DIR/requirements.txt" +else + echo "Warning: requirements.txt not found at $API_DIR/requirements.txt" +fi + +# Install development requirements if they exist +if [ -f "$API_DIR/requirements-dev.txt" ]; then + echo "Installing development requirements from $API_DIR/requirements-dev.txt..." + pip install -r "$API_DIR/requirements-dev.txt" +fi + +# Install AWS SAM CLI if not already installed +if ! command -v sam &> /dev/null; then + echo "AWS SAM CLI not found. Installing..." + pip install aws-sam-cli +else + echo "AWS SAM CLI already installed: $(sam --version)" +fi + +echo "" +echo "Virtual environment setup complete!" +echo "" +echo "To activate the virtual environment, run:" +echo " source $VENV_PATH/bin/activate" +echo "" +echo "To start the development environment, run:" +echo " cd gogen-api" +echo " ./start_dev.sh" +echo "" + +# Add a note about automatically activating the environment +echo "Tip: Add this to your .bashrc or .zshrc to automatically activate" +echo "the environment when entering the project directory:" +echo "" +echo 'function cd() {' +echo ' builtin cd "$@"' +echo ' if [[ -d .pyvenv ]] && [[ -f .pyvenv/bin/activate ]]; then' +echo ' source .pyvenv/bin/activate' +echo ' fi' +echo '}' \ No newline at end of file From 670a5a377770e3856a8fdc8c1f27c52245180f39 Mon Sep 17 00:00:00 2001 From: Clint Sharp Date: Mon, 17 Mar 2025 22:11:21 -0700 Subject: [PATCH 09/51] Adding new UI (#53) * First draft of a UI. Broken WASM implementation but otherwise working. * Working UI implementation for WASM * Refactored gogenWasm.ts for better readability * Numerous UI updates and fixes. * First pass at unit tests * Added unit tests for Pages and missing component tests * Tests for the backend and App.tsx * Updated loading spinner and default execute options * Getting ready for deployment. Created staging environment. Updated code to fix CORS problems. Added support for multiple environments. --- .cursor/rules/ui-api.mdc | 97 + .cursor/rules/ui-component.mdc | 83 + .cursor/rules/ui-general.mdc | 43 + .cursor/rules/ui-styling.mdc | 45 + .cursor/rules/ui-testing.mdc | 96 + .github/workflows/ci.yml | 58 +- .gitignore | 7 +- go.mod | 8 +- go.sum | 8 +- gogen-api/api/cors_utils.py | 50 + gogen-api/api/db_utils.py | 12 + gogen-api/api/get.py | 51 +- gogen-api/api/list.py | 20 +- gogen-api/api/s3_utils.py | 23 +- gogen-api/api/search.py | 20 +- gogen-api/api/upsert.py | 20 +- gogen-api/deploy_lambdas.sh | 191 +- gogen-api/template.yaml | 140 +- ui/.env | 1 + ui/.env.development | 1 + ui/.env.production | 1 + ui/.env.staging | 1 + ui/README.md | 117 + ui/REQUIREMENTS.md | 88 + ui/SUMMARY.md | 82 + ui/deploy_ui.sh | 63 + ui/gogen-api-spec.yaml | 148 + ui/index.html | 15 + ui/jest.config.ts | 33 + ui/package-lock.json | 8457 +++++++++++++++++ ui/package.json | 51 + ui/postcss.config.js | 6 + ui/public/favicon.svg | 4 + ui/public/wasm_exec.js | 575 ++ ui/src/App.test.tsx | 69 + ui/src/App.tsx | 21 + ui/src/api/__mocks__/api.ts | 36 + ui/src/api/gogenApi.test.ts | 137 + ui/src/api/gogenApi.ts | 66 + ui/src/api/gogenWasm.test.ts | 366 + ui/src/api/gogenWasm.ts | 270 + ui/src/components/ConfigurationList.test.tsx | 72 + ui/src/components/ConfigurationList.tsx | 148 + ui/src/components/ExecutionComponent.test.tsx | 151 + ui/src/components/ExecutionComponent.tsx | 301 + ui/src/components/Footer.test.tsx | 55 + ui/src/components/Footer.tsx | 41 + ui/src/components/Header.test.tsx | 48 + ui/src/components/Header.tsx | 18 + ui/src/components/Hero.test.tsx | 50 + ui/src/components/Hero.tsx | 14 + ui/src/components/Layout.test.tsx | 41 + ui/src/components/Layout.tsx | 19 + ui/src/components/LoadingSpinner.test.tsx | 11 + ui/src/components/LoadingSpinner.tsx | 55 + ui/src/config.ts | 8 + ui/src/index.css | 59 + ui/src/main.tsx | 10 + ui/src/pages/ConfigurationDetailPage.test.tsx | 95 + ui/src/pages/ConfigurationDetailPage.tsx | 141 + ui/src/pages/HomePage.test.tsx | 121 + ui/src/pages/HomePage.tsx | 45 + ui/src/pages/NotFoundPage.test.tsx | 42 + ui/src/pages/NotFoundPage.tsx | 18 + ui/src/setupTests.ts | 28 + ui/src/utils/test-utils.tsx | 16 + ui/src/vite-env.d.ts | 12 + ui/tailwind.config.js | 25 + ui/tsconfig.json | 27 + ui/tsconfig.node.json | 10 + ui/vite.config.ts | 46 + vendor/github.com/sirupsen/logrus/README.md | 2 +- .../sirupsen/logrus/terminal_check_bsd.go | 2 +- .../sirupsen/logrus/terminal_check_unix.go | 2 + .../sirupsen/logrus/terminal_check_wasi.go | 8 + .../sirupsen/logrus/terminal_check_wasip1.go | 8 + vendor/modules.txt | 6 +- 77 files changed, 13141 insertions(+), 194 deletions(-) create mode 100644 .cursor/rules/ui-api.mdc create mode 100644 .cursor/rules/ui-component.mdc create mode 100644 .cursor/rules/ui-general.mdc create mode 100644 .cursor/rules/ui-styling.mdc create mode 100644 .cursor/rules/ui-testing.mdc create mode 100644 gogen-api/api/cors_utils.py create mode 100644 ui/.env create mode 100644 ui/.env.development create mode 100644 ui/.env.production create mode 100644 ui/.env.staging create mode 100644 ui/README.md create mode 100644 ui/REQUIREMENTS.md create mode 100644 ui/SUMMARY.md create mode 100755 ui/deploy_ui.sh create mode 100644 ui/gogen-api-spec.yaml create mode 100644 ui/index.html create mode 100644 ui/jest.config.ts create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/postcss.config.js create mode 100644 ui/public/favicon.svg create mode 100644 ui/public/wasm_exec.js create mode 100644 ui/src/App.test.tsx create mode 100644 ui/src/App.tsx create mode 100644 ui/src/api/__mocks__/api.ts create mode 100644 ui/src/api/gogenApi.test.ts create mode 100644 ui/src/api/gogenApi.ts create mode 100644 ui/src/api/gogenWasm.test.ts create mode 100644 ui/src/api/gogenWasm.ts create mode 100644 ui/src/components/ConfigurationList.test.tsx create mode 100644 ui/src/components/ConfigurationList.tsx create mode 100644 ui/src/components/ExecutionComponent.test.tsx create mode 100644 ui/src/components/ExecutionComponent.tsx create mode 100644 ui/src/components/Footer.test.tsx create mode 100644 ui/src/components/Footer.tsx create mode 100644 ui/src/components/Header.test.tsx create mode 100644 ui/src/components/Header.tsx create mode 100644 ui/src/components/Hero.test.tsx create mode 100644 ui/src/components/Hero.tsx create mode 100644 ui/src/components/Layout.test.tsx create mode 100644 ui/src/components/Layout.tsx create mode 100644 ui/src/components/LoadingSpinner.test.tsx create mode 100644 ui/src/components/LoadingSpinner.tsx create mode 100644 ui/src/config.ts create mode 100644 ui/src/index.css create mode 100644 ui/src/main.tsx create mode 100644 ui/src/pages/ConfigurationDetailPage.test.tsx create mode 100644 ui/src/pages/ConfigurationDetailPage.tsx create mode 100644 ui/src/pages/HomePage.test.tsx create mode 100644 ui/src/pages/HomePage.tsx create mode 100644 ui/src/pages/NotFoundPage.test.tsx create mode 100644 ui/src/pages/NotFoundPage.tsx create mode 100644 ui/src/setupTests.ts create mode 100644 ui/src/utils/test-utils.tsx create mode 100644 ui/src/vite-env.d.ts create mode 100644 ui/tailwind.config.js create mode 100644 ui/tsconfig.json create mode 100644 ui/tsconfig.node.json create mode 100644 ui/vite.config.ts create mode 100644 vendor/github.com/sirupsen/logrus/terminal_check_wasi.go create mode 100644 vendor/github.com/sirupsen/logrus/terminal_check_wasip1.go diff --git a/.cursor/rules/ui-api.mdc b/.cursor/rules/ui-api.mdc new file mode 100644 index 0000000..3fc5333 --- /dev/null +++ b/.cursor/rules/ui-api.mdc @@ -0,0 +1,97 @@ +--- +description: UI API Rules +globs: *.tsx, *.ts +alwaysApply: false +--- +# Gogen UI API Integration Rules + +This document outlines the standards for API integration in the Gogen UI project. + +## API Client Structure + +- Use a centralized API client for all API calls +- Define TypeScript interfaces for all API responses +- Use axios for HTTP requests +- Configure base URL and default headers in a single place + +## Error Handling + +- Implement proper error handling for all API calls +- Log errors to the console for debugging +- Return meaningful error messages to the UI +- Use try/catch blocks for async operations + +## Mock Data + +- Create mock data for development and testing +- Ensure mock data matches the shape of real API responses +- Use mock data when the API is not available +- Document the structure of mock data + +## Example API Client + +```tsx +import axios from 'axios'; + +// Define the base URL for the API +const API_BASE_URL = 'https://api.gogen.io/v1'; + +// Create an axios instance with default config +const apiClient = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Define interfaces for API responses +export interface ConfigurationSummary { + gogen: string; + description: string; +} + +export interface Configuration extends ConfigurationSummary { + yaml?: string; + samples?: any[]; + raters?: any[]; + mix?: any[]; + generators?: any[]; + global?: any; + templates?: any[]; +} + +// API functions +export const gogenApi = { + // Get a list of all configurations + listConfigurations: async (): Promise => { + try { + const response = await apiClient.get('/list'); + return response.data.Items || []; + } catch (error) { + console.error('Error fetching configurations:', error); + throw error; + } + }, + + // Get a specific configuration by name + getConfiguration: async (configName: string): Promise => { + try { + const response = await apiClient.get(`/get/${configName}`); + return response.data.Item || {}; + } catch (error) { + console.error(`Error fetching configuration ${configName}:`, error); + throw error; + } + }, +}; + +export default gogenApi; +``` + +## API Integration in Components + +- Use the useEffect hook for data fetching +- Implement loading states for all data-dependent components +- Handle API errors gracefully +- Use the API client for all API calls +- Avoid making API calls directly in components \ No newline at end of file diff --git a/.cursor/rules/ui-component.mdc b/.cursor/rules/ui-component.mdc new file mode 100644 index 0000000..9197b30 --- /dev/null +++ b/.cursor/rules/ui-component.mdc @@ -0,0 +1,83 @@ +--- +description: UI Component Rules +globs: *.tsx +alwaysApply: false +--- +# Gogen UI Component Rules + +This document outlines the standards for creating and using React components in the Gogen UI project. + +## Component Structure + +- Use functional components with hooks instead of class components +- Keep components small and focused on a single responsibility +- Extract reusable logic into custom hooks +- Use TypeScript for component props and state + +## Props and State + +- Define prop types using TypeScript interfaces +- Use default props when appropriate +- Destructure props in function parameters +- Use the useState hook for component state +- Use the useEffect hook for side effects + +## Data Fetching + +- Use the useEffect hook for data fetching +- Implement loading states for all data-dependent components +- Implement error handling for all API calls +- Use try/catch blocks for async operations +- Display user-friendly error messages + +## Example Component Structure + +```tsx +import { useState, useEffect } from 'react'; +import { SomeType } from '../types'; +import LoadingSpinner from './LoadingSpinner'; + +interface MyComponentProps { + id: string; + title?: string; + onAction: (id: string) => void; +} + +const MyComponent = ({ id, title = 'Default Title', onAction }: MyComponentProps) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + // Fetch data here + setData(/* fetched data */); + setError(null); + } catch (err) { + setError('Failed to fetch data'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [id]); + + if (loading) return ; + if (error) return
{error}
; + if (!data) return
No data available
; + + return ( +
+

{title}

+ {/* Render component content */} + +
+ ); +}; + +export default MyComponent; +``` \ No newline at end of file diff --git a/.cursor/rules/ui-general.mdc b/.cursor/rules/ui-general.mdc new file mode 100644 index 0000000..c1a5886 --- /dev/null +++ b/.cursor/rules/ui-general.mdc @@ -0,0 +1,43 @@ +--- +description: +globs: ui/** +alwaysApply: false +--- +# Gogen UI Cursor Rules + +This document outlines the standard rules for developing the Gogen UI project using Cursor. + +## General Rules + +- Keep components modular and focused on a single responsibility +- Use TypeScript interfaces for all data structures +- Document code with clear comments +- Update SUMMARY.md after completing significant features + +## Development Workflow + +- After running the development server, verify UI is working as expected +- Use mock data during development until the actual API is ready +- Implement proper error handling for all async operations +- Test components in isolation before integration + +## File Organization + +- Place components in the `src/components` directory +- Place pages in the `src/pages` directory +- Place API clients in the `src/api` directory +- Place utility functions in the `src/utils` directory +- Place TypeScript interfaces in the `src/types` directory + +## Development Server Verification + +- After running the development server, always prompt the user to verify if the UI is working as expected +- Example prompt: "Is the UI loading correctly at http://localhost:3000? Are there any visual issues or console errors you're seeing?" +- Address any issues reported by the user before proceeding with further development + +## See Also + +- [Styling Rules](mdc:ui-styling.mdc) +- [Component Rules](mdc:ui-component.mdc) +- [API Integration Rules](mdc:ui-api.mdc) +- [Testing Rules](mdc:cursor-rules-testing.mdc) \ No newline at end of file diff --git a/.cursor/rules/ui-styling.mdc b/.cursor/rules/ui-styling.mdc new file mode 100644 index 0000000..dce5a03 --- /dev/null +++ b/.cursor/rules/ui-styling.mdc @@ -0,0 +1,45 @@ +--- +description: UI Styling rules +globs: *.tsx +alwaysApply: false +--- +# Gogen UI Styling Rules + +This document outlines the styling standards for the Gogen UI project. + +## Tailwind CSS Usage + +- Prefer standard Tailwind utility classes over custom classes when possible +- When custom classes are needed, define them in `index.css` using the `@apply` directive +- Document all custom colors and component classes in the README +- Use consistent spacing and sizing utilities + +## Color Scheme + +- Use the established color scheme defined in `tailwind.config.js` +- For primary actions: blue-800 backgrounds with white text +- For secondary actions: cyan-400 backgrounds with blue-900 text +- For UI structure: gray-100 for backgrounds, white for cards, gray-800 for footer +- For text: gray-800 for headings, gray-600 for body text + +## Component Styling + +- Use the following classes for buttons: + - Primary: `bg-blue-800 text-white hover:bg-blue-700` + - Secondary: `bg-cyan-400 text-blue-900 hover:bg-cyan-300` + - Outline: `border border-blue-800 text-blue-800 hover:bg-blue-50` + +- Use the following classes for cards: + - `bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow` + +- Use the following classes for tables: + - Table: `min-w-full bg-white rounded-lg overflow-hidden shadow-md` + - Header: `bg-gray-100 text-gray-700` + - Rows: `hover:bg-gray-50 divide-y divide-gray-200` + +## Responsive Design + +- Use responsive utility classes (sm:, md:, lg:) for different screen sizes +- Ensure tables have overflow handling for smaller screens +- Use the container class with appropriate padding: `container mx-auto px-4` +- Design mobile-first, then add breakpoints for larger screens \ No newline at end of file diff --git a/.cursor/rules/ui-testing.mdc b/.cursor/rules/ui-testing.mdc new file mode 100644 index 0000000..5e45353 --- /dev/null +++ b/.cursor/rules/ui-testing.mdc @@ -0,0 +1,96 @@ +--- +description: Rules for how to write UI Tests +globs: *.tsx +alwaysApply: false +--- + # Gogen UI Testing Rules + +This document outlines the testing standards for the Gogen UI project. + +## Testing Framework + +- Use Jest as the test runner +- Use React Testing Library for component testing +- Place test files next to the files they test with a `.test.tsx` extension + +## Test Coverage + +- Write tests for all utility functions +- Write tests for all API clients +- Write tests for all reusable components +- Focus on testing behavior, not implementation details + +## Testing Components + +- Test that components render without errors +- Test that components display the correct content +- Test user interactions (clicks, form submissions, etc.) +- Test loading and error states + +## Mocking + +- Mock API calls in component tests +- Use Jest mock functions for callbacks and event handlers +- Create mock data that matches the shape of real data + +## Example Component Test + +```tsx +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import MyComponent from './MyComponent'; +import { fetchData } from '../api/dataApi'; + +// Mock the API module +jest.mock('../api/dataApi'); + +describe('MyComponent', () => { + const mockData = { id: '123', name: 'Test Item' }; + const mockOnAction = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders loading state initially', () => { + (fetchData as jest.Mock).mockResolvedValueOnce(mockData); + render(); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + test('renders data when loaded', async () => { + (fetchData as jest.Mock).mockResolvedValueOnce(mockData); + render(); + + await waitFor(() => { + expect(screen.getByText('Test Item')).toBeInTheDocument(); + }); + }); + + test('renders error message when API fails', async () => { + (fetchData as jest.Mock).mockRejectedValueOnce(new Error('API Error')); + render(); + + await waitFor(() => { + expect(screen.getByText('Failed to fetch data')).toBeInTheDocument(); + }); + }); + + test('calls onAction when button is clicked', async () => { + (fetchData as jest.Mock).mockResolvedValueOnce(mockData); + render(); + + await waitFor(() => { + fireEvent.click(screen.getByText('Perform Action')); + expect(mockOnAction).toHaveBeenCalledWith('123'); + }); + }); +}); +``` + +## Development Server Verification + +- After running the development server, always prompt the user to verify if the UI is working as expected +- Example prompt: "Is the UI loading correctly at http://localhost:3000? Are there any visual issues or console errors you're seeing?" +- Address any issues reported by the user before proceeding with further development \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 062faf8..3820833 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ master ] + branches: [ master, dev ] pull_request: jobs: @@ -32,16 +32,16 @@ jobs: $HOME/gopath/bin/goveralls -v -service=github - name: Build Project - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' run: make GOBIN=$HOME/gopath/bin build - name: Build Docker Image - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' run: docker build -t clintsharp/gogen . - # Deployment: These steps run only on the master branch. + # Deployment steps run on both master and dev branches - name: Configure AWS Credentials - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -49,8 +49,13 @@ jobs: aws-region: us-west-1 - name: Deploy Build Artifacts to S3 - if: github.ref == 'refs/heads/master' - run: aws s3 sync build s3://gogen-artifacts --delete + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' + run: | + if [ "${{ github.ref }}" = "refs/heads/master" ]; then + aws s3 sync build s3://gogen-artifacts --delete + else + aws s3 sync build s3://gogen-artifacts-staging --delete + fi - name: Run Docker Push Script if: github.ref == 'refs/heads/master' @@ -62,7 +67,7 @@ jobs: deploy-lambdas: runs-on: ubuntu-latest needs: build - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' steps: - name: Checkout code uses: actions/checkout@v2 @@ -92,5 +97,38 @@ jobs: run: | source .pyvenv/bin/activate cd gogen-api - # Run the deployment script - ./deploy_lambdas.sh + if [ "${{ github.ref }}" = "refs/heads/master" ]; then + ./deploy_lambdas.sh + else + ./deploy_lambdas.sh -e staging + fi + + deploy-ui: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-1 + + - name: Deploy UI + run: | + chmod +x ui/deploy_ui.sh + if [ "${{ github.ref }}" = "refs/heads/master" ]; then + ui/deploy_ui.sh + else + ui/deploy_ui.sh -e staging + fi + diff --git a/.gitignore b/.gitignore index 46d0879..99d74bd 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,9 @@ roveralls* .pyvenv gogen-api/__pycache__ gogen-api/build -ui/* \ No newline at end of file +ui/node_modules/* +ui/dist/* +ui/build/* +ui/coverage/* +ui/public/gogen.wasm +ui/.vite \ No newline at end of file diff --git a/go.mod b/go.mod index d39cf80..ae51610 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/coccyx/gogen -go 1.22.0 +go 1.23.0 -toolchain go1.22.2 +toolchain go1.24.1 require ( github.com/cactus/gostrftime v1.0.2 @@ -17,7 +17,7 @@ require ( github.com/pkg/profile v1.7.0 github.com/satori/go.uuid v1.2.0 github.com/segmentio/kafka-go v0.4.47 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.4-0.20241118143825-d1e633264448 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.10.0 github.com/yuin/gopher-lua v1.1.1 @@ -41,7 +41,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 43075aa..858f70d 100644 --- a/go.sum +++ b/go.sum @@ -84,8 +84,8 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0= github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4-0.20241118143825-d1e633264448 h1:4T/wluVIsyQ0Kqamo3he0Q0FhZG7CBd5LJgb4KOmftM= +github.com/sirupsen/logrus v1.9.4-0.20241118143825-d1e633264448/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -138,8 +138,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/gogen-api/api/cors_utils.py b/gogen-api/api/cors_utils.py new file mode 100644 index 0000000..b6b26a3 --- /dev/null +++ b/gogen-api/api/cors_utils.py @@ -0,0 +1,50 @@ +import os +import json +import decimal +from typing import Any, Dict, Optional, Union + +def decimal_default(obj): + """Helper function to convert Decimal to float for JSON serialization.""" + if isinstance(obj, decimal.Decimal): + return float(obj) + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + +def get_cors_headers() -> Dict[str, str]: + """Get the CORS headers based on the environment.""" + env = os.environ.get('ENVIRONMENT', 'dev') + origin = 'https://gogen.io' if env == 'prod' else 'https://staging.gogen.io' + + return { + 'Access-Control-Allow-Origin': origin, + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Requested-With', + 'Access-Control-Allow-Credentials': 'true', + 'Content-Type': 'application/json' + } + +def cors_response(status_code: Union[int, str], body: Optional[Any] = None, additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]: + """ + Create a response with CORS headers. + + Args: + status_code: HTTP status code + body: Response body (will be JSON serialized if not a string) + additional_headers: Additional headers to include in the response + + Returns: + Dict containing the complete response with CORS headers + """ + headers = get_cors_headers() + if additional_headers: + headers.update(additional_headers) + + if isinstance(body, str): + response_body = body + else: + response_body = json.dumps(body, default=decimal_default) if body is not None else '' + + return { + 'statusCode': str(status_code), + 'headers': headers, + 'body': response_body + } \ No newline at end of file diff --git a/gogen-api/api/db_utils.py b/gogen-api/api/db_utils.py index c2faa24..15243b2 100644 --- a/gogen-api/api/db_utils.py +++ b/gogen-api/api/db_utils.py @@ -5,6 +5,18 @@ logger = setup_logger(__name__) +def get_table_name(): + """ + Get the DynamoDB table name based on environment + """ + env = os.environ.get('ENVIRONMENT', 'dev') + if env == 'staging': + return 'gogen-staging' + elif env == 'prod': + return 'gogen' + else: + return 'gogen' # Default to dev table name + def get_dynamodb_client(): """ Get a DynamoDB client - uses local endpoint if running locally diff --git a/gogen-api/api/get.py b/gogen-api/api/get.py index 43fc20e..5bb8474 100644 --- a/gogen-api/api/get.py +++ b/gogen-api/api/get.py @@ -3,28 +3,19 @@ import urllib.request import urllib.error from boto3.dynamodb.conditions import Key, Attr -from db_utils import get_dynamodb_client +from db_utils import get_dynamodb_client, get_table_name from s3_utils import download_config +from cors_utils import cors_response from logger import setup_logger logger = setup_logger(__name__) logger.info('Loading function') -def decimal_default(obj): - if isinstance(obj, decimal.Decimal): - return float(obj) - raise TypeError - - def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': str(err) if err else json.dumps(res, default=decimal_default), - 'headers': { - 'Content-Type': 'application/json', - }, - } + if err: + return cors_response(400, err) + return cors_response(200, res) def fetch_gist_content(gist_id): @@ -130,26 +121,25 @@ def fetch_gist_content(gist_id): def lambda_handler(event, context): logger.debug(f"Received event: {json.dumps(event)}") + + # Handle OPTIONS requests for CORS + if event.get('httpMethod') == 'OPTIONS': + return cors_response(200, {'message': 'OK'}) + q = event['pathParameters']['proxy'] logger.debug(f"Query: {q}") - table = get_dynamodb_client().Table('gogen') + table = get_dynamodb_client().Table(get_table_name()) response = table.get_item(Key={"gogen": q}) if 'Item' not in response: logger.error(f"No item found for query: {q}") - return { - 'statusCode': '404', - 'body': f'Could not find Gogen: {q}', - } + return cors_response(404, f'Could not find Gogen: {q}') item = response['Item'] if 'gogen' not in item: logger.error(f"Item found but missing 'gogen' key for query: {q}") - return { - 'statusCode': '404', - 'body': f'Could not find Gogen: {q}', - } + return cors_response(404, f'Could not find Gogen: {q}') # Try to fetch the configuration from S3 first if 's3Path' in item: @@ -160,10 +150,7 @@ def lambda_handler(event, context): logger.debug(f"Successfully added config content from S3 for query: {q}") else: logger.error(f"Failed to fetch config content from S3 for query: {q}") - return { - 'statusCode': '500', - 'body': f'Failed to fetch configuration from S3 for: {q}', - } + return cors_response(500, f'Failed to fetch configuration from S3 for: {q}') # For backward compatibility, try to fetch from GitHub gist if s3Path is not present elif 'gistID' in item: logger.warning(f"Using legacy gistID: {item['gistID']} for query: {q}. This will be deprecated.") @@ -173,16 +160,10 @@ def lambda_handler(event, context): logger.debug(f"Successfully added config content from GitHub gist for query: {q}") else: logger.error(f"Failed to fetch config content from GitHub gist for query: {q}") - return { - 'statusCode': '500', - 'body': f'Failed to fetch configuration from GitHub gist for: {q}', - } + return cors_response(500, f'Failed to fetch configuration from GitHub gist for: {q}') else: logger.error(f"No s3Path or gistID found in item for query: {q}") - return { - 'statusCode': '500', - 'body': f'Configuration {q} does not have a valid storage location.', - } + return cors_response(500, f'Configuration {q} does not have a valid storage location.') response['Item'] = item return respond(None, response) diff --git a/gogen-api/api/list.py b/gogen-api/api/list.py index 930b6c9..ab1e0ec 100644 --- a/gogen-api/api/list.py +++ b/gogen-api/api/list.py @@ -1,24 +1,24 @@ import json -from db_utils import get_dynamodb_client +from db_utils import get_dynamodb_client, get_table_name +from cors_utils import cors_response from logger import setup_logger logger = setup_logger(__name__) logger.info('Loading function') def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': str(err) if err else json.dumps(res), - 'headers': { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - } + if err: + return cors_response(400, str(err)) + return cors_response(200, res) def lambda_handler(event, context): + # Handle OPTIONS requests for CORS + if event.get('httpMethod') == 'OPTIONS': + return cors_response(200, {'message': 'OK'}) + try: logger.debug(f"Received event: {json.dumps(event, indent=2)}") - table = get_dynamodb_client().Table('gogen') + table = get_dynamodb_client().Table(get_table_name()) # Use pagination to handle large datasets items = [] diff --git a/gogen-api/api/s3_utils.py b/gogen-api/api/s3_utils.py index 4a3cd36..06b92c1 100644 --- a/gogen-api/api/s3_utils.py +++ b/gogen-api/api/s3_utils.py @@ -38,30 +38,39 @@ def get_s3_client(): raise return client else: - # Production environment - use AWS credentials from environment or instance profile - logger.info("Configuring S3 client for production environment") + # Production/Staging environment - use AWS credentials from environment or instance profile + env = os.environ.get('ENVIRONMENT', 'dev') + logger.info(f"Configuring S3 client for {env} environment") region = os.environ.get('AWS_REGION', 'us-east-1') try: client = boto3.resource('s3', region_name=region, config=config) - logger.info(f"Successfully created S3 client for region {region}") + logger.info(f"Successfully created S3 client for region {region} in {env} environment") return client except Exception as e: - logger.error(f"Failed to create production S3 client: {str(e)}") + logger.error(f"Failed to create {env} S3 client: {str(e)}") raise def get_config_bucket(): """ - Get the gogen-configs bucket + Get the gogen-configs bucket based on environment """ try: s3 = get_s3_client() - bucket_name = os.environ.get('CONFIG_BUCKET_NAME', 'gogen-configs') + env = os.environ.get('ENVIRONMENT', 'dev') + if env == 'staging': + bucket_name = 'gogen-configs-staging' + elif env == 'prod': + bucket_name = 'gogen-configs' + else: + bucket_name = os.environ.get('CONFIG_BUCKET_NAME', 'gogen-configs') # Default to dev bucket name + + logger.info(f"Using S3 bucket: {bucket_name} for environment: {env}") bucket = s3.Bucket(bucket_name) try: # Verify bucket exists by trying to load bucket properties bucket.meta.client.head_bucket(Bucket=bucket_name) - logger.info(f"Successfully connected to bucket: {bucket_name}") + logger.info(f"Successfully connected to bucket: {bucket_name} in {env} environment") except bucket.meta.client.exceptions.ClientError as e: error_code = e.response['Error']['Code'] if error_code == '403': diff --git a/gogen-api/api/search.py b/gogen-api/api/search.py index 0e13c5f..3058ba4 100644 --- a/gogen-api/api/search.py +++ b/gogen-api/api/search.py @@ -1,6 +1,7 @@ import json from boto3.dynamodb.conditions import Key, Attr -from db_utils import get_dynamodb_client +from db_utils import get_dynamodb_client, get_table_name +from cors_utils import cors_response from logger import setup_logger logger = setup_logger(__name__) @@ -8,17 +9,16 @@ def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': str(err) if err else json.dumps(res), - 'headers': { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - } + if err: + return cors_response(400, str(err)) + return cors_response(200, res) def lambda_handler(event, context): + # Handle OPTIONS requests for CORS + if event.get('httpMethod') == 'OPTIONS': + return cors_response(200, {'message': 'OK'}) + try: logger.debug(f"Received event: {json.dumps(event, indent=2)}") @@ -32,7 +32,7 @@ def lambda_handler(event, context): logger.info(f"Processing search query: {q}") - table = get_dynamodb_client().Table('gogen') + table = get_dynamodb_client().Table(get_table_name()) # Use pagination to handle large datasets items = [] diff --git a/gogen-api/api/upsert.py b/gogen-api/api/upsert.py index 84d8c50..09b58de 100644 --- a/gogen-api/api/upsert.py +++ b/gogen-api/api/upsert.py @@ -1,8 +1,9 @@ import json import http.client from boto3.dynamodb.conditions import Key, Attr -from db_utils import get_dynamodb_client +from db_utils import get_dynamodb_client, get_table_name from s3_utils import upload_config +from cors_utils import cors_response from logger import setup_logger logger = setup_logger(__name__) @@ -10,14 +11,9 @@ def respond(err, res=None): - return { - 'statusCode': '400' if err else '200', - 'body': str(err) if err else json.dumps(res), - 'headers': { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - } + if err: + return cors_response(400, str(err)) + return cors_response(200, res) def validate_github_token(token): @@ -46,6 +42,10 @@ def validate_github_token(token): def lambda_handler(event, context): + # Handle OPTIONS requests for CORS + if event.get('httpMethod') == 'OPTIONS': + return cors_response(200, {'message': 'OK'}) + try: logger.debug(f"Received event: {json.dumps(event, indent=2)}") @@ -114,7 +114,7 @@ def lambda_handler(event, context): logger.info(f"Processing upsert for item: {validated_body}") # Store in DynamoDB - table = get_dynamodb_client().Table('gogen') + table = get_dynamodb_client().Table(get_table_name()) logger.debug(f"Attempting to upsert item to DynamoDB: {validated_body}") response = table.put_item( diff --git a/gogen-api/deploy_lambdas.sh b/gogen-api/deploy_lambdas.sh index 3035bf6..69c5be2 100755 --- a/gogen-api/deploy_lambdas.sh +++ b/gogen-api/deploy_lambdas.sh @@ -5,14 +5,66 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" -# Configuration -LAMBDA_DIR="$SCRIPT_DIR/api" -BUILD_DIR="$SCRIPT_DIR/build" -REGION="us-east-1" # Change this to your AWS region -RUNTIME="python3.13" +# Parse command line arguments +ENVIRONMENT="prod" # Default to production +while getopts "e:" opt; do + case $opt in + e) ENVIRONMENT="$OPTARG" + ;; + \?) echo "Invalid option -$OPTARG" >&2 + ;; + esac +done -# Use environment variable if set, otherwise use the default value -ROLE_ARN=${LAMBDA_ROLE_ARN:-$ROLE_ARN} +# Validate environment +if [[ "$ENVIRONMENT" != "prod" && "$ENVIRONMENT" != "staging" ]]; then + echo "Invalid environment: $ENVIRONMENT. Must be 'prod' or 'staging'" + exit 1 +fi + +# Set the S3 bucket name based on environment +if [ "$ENVIRONMENT" = "prod" ]; then + S3_BUCKET="gogen-artifacts" +else + S3_BUCKET="gogen-artifacts-staging" +fi + +# Ensure the S3 bucket exists +ensure_s3_bucket() { + local bucket=$1 + if ! aws s3api head-bucket --bucket "$bucket" 2>/dev/null; then + echo "S3 bucket $bucket does not exist or is not accessible" + exit 1 + fi + echo "Using S3 bucket: $bucket for deployment artifacts" +} + +# Check S3 bucket +ensure_s3_bucket "$S3_BUCKET" + +# Get the appropriate role ARN based on environment +get_role_arn() { + local env=$1 + local role_name + + if [ "$env" = "prod" ]; then + role_name="gogen_lambda" + else + role_name="gogen_lambda_staging" + fi + + # Get the role ARN + role_arn=$(aws iam get-role --role-name "$role_name" --query 'Role.Arn' --output text) + if [ -z "$role_arn" ]; then + echo "Failed to get ARN for role: $role_name" >&2 + exit 1 + fi + echo "$role_arn" +} + +# Get the role ARN +ROLE_ARN=$(get_role_arn "$ENVIRONMENT") +echo "Using role ARN: $ROLE_ARN" # Check if virtual environment exists and activate it VENV_PATH="$PROJECT_ROOT/.pyvenv" @@ -38,106 +90,59 @@ else if [ -f "$SCRIPT_DIR/requirements.txt" ]; then echo "Installing requirements from $SCRIPT_DIR/requirements.txt..." pip install -r "$SCRIPT_DIR/requirements.txt" - else - echo "Installing boto3 and botocore..." - pip install boto3 botocore fi - # Install AWS CLI if needed - if ! command -v aws &> /dev/null; then - echo "Installing AWS CLI..." - pip install awscli + # Install AWS SAM CLI if needed + if ! command -v sam &> /dev/null; then + echo "Installing AWS SAM CLI..." + pip install aws-sam-cli fi fi fi -# Create build directory if it doesn't exist -mkdir -p $BUILD_DIR +# Validate AWS credentials are configured +if ! aws sts get-caller-identity &> /dev/null; then + echo "AWS credentials are not configured. Please run 'aws configure' first." + exit 1 +fi -# Function to package and deploy a Lambda function -deploy_lambda() { - local function_name="Gogen$1" - local handler_file="${1,,}.py" # Convert to lowercase - local handler_name="${1,,}.lambda_handler" - local zip_file="$BUILD_DIR/${function_name}.zip" - - echo "Packaging $function_name..." - - # Create a temporary directory for packaging - local temp_dir=$(mktemp -d) - - # Copy the handler file and dependencies - cp "$LAMBDA_DIR/$handler_file" "$temp_dir/" - cp "$LAMBDA_DIR/db_utils.py" "$temp_dir/" - cp "$LAMBDA_DIR/logger.py" "$temp_dir/" - - # Copy s3_utils.py if needed by this function - if [[ "$1" == "Get" || "$1" == "Upsert" ]]; then - cp "$LAMBDA_DIR/s3_utils.py" "$temp_dir/" - fi - - # Install dependencies into the package - if [ -f "$SCRIPT_DIR/requirements.txt" ]; then - echo "Installing dependencies from requirements.txt..." - pip install -r "$SCRIPT_DIR/requirements.txt" -t "$temp_dir/" --no-cache-dir - else - echo "requirements.txt not found, installing boto3 and botocore..." - pip install boto3 botocore -t "$temp_dir/" --no-cache-dir - fi - - # Create zip file - echo "Creating zip file: $zip_file" - (cd "$temp_dir" && zip -r "$zip_file" .) +# Get or create ACM certificate ARN +get_certificate_arn() { + # Check if certificate exists for *.gogen.io + CERT_ARN=$(aws acm list-certificates --query "CertificateSummaryList[?DomainName=='*.gogen.io'].CertificateArn" --output text) - # Check if Lambda function exists - echo "Checking if Lambda function $function_name exists..." - if aws lambda get-function --function-name "$function_name" --region "$REGION" 2>&1 | grep -q "Function not found"; then - # Create new Lambda function - echo "Creating new Lambda function: $function_name" - aws lambda create-function \ - --function-name "$function_name" \ - --runtime "$RUNTIME" \ - --role "$ROLE_ARN" \ - --handler "$handler_name" \ - --zip-file "fileb://$zip_file" \ - --region "$REGION" - else - # Update existing Lambda function - echo "Updating existing Lambda function: $function_name" - aws lambda update-function-code \ - --function-name "$function_name" \ - --zip-file "fileb://$zip_file" \ - --region "$REGION" + if [ -z "$CERT_ARN" ]; then + echo "No certificate found for *.gogen.io" + exit 1 fi - # Clean up - rm -rf "$temp_dir" - - echo "$function_name deployment complete!" + echo $CERT_ARN } -# Validate AWS CLI is installed -if ! command -v aws &> /dev/null; then - echo "AWS CLI is not installed. Installing..." - pip install awscli -fi - -# Validate AWS credentials are configured -if ! aws sts get-caller-identity &> /dev/null; then - echo "AWS credentials are not configured. Please run 'aws configure' first." +# Get certificate ARN +CERT_ARN=$(get_certificate_arn) +if [ -z "$CERT_ARN" ]; then + echo "Failed to get certificate ARN" exit 1 fi -# Check if ROLE_ARN is set -if [ -z "$ROLE_ARN" ]; then - echo "Please set the LAMBDA_ROLE_ARN environment variable or the ROLE_ARN variable in this script." - exit 1 -fi +echo "Using certificate ARN: $CERT_ARN" + +# Build the SAM application +echo "Building SAM application..." +sam build --use-container -# Deploy each Lambda function -deploy_lambda "Get" -deploy_lambda "List" -deploy_lambda "Search" -deploy_lambda "Upsert" +# Deploy the SAM application +echo "Deploying SAM application for $ENVIRONMENT environment..." +sam deploy \ + --stack-name "gogen-api-${ENVIRONMENT}" \ + --s3-bucket "$S3_BUCKET" \ + --parameter-overrides \ + Environment=$ENVIRONMENT \ + LambdaRoleArn=$ROLE_ARN \ + CertificateArn=$CERT_ARN \ + --capabilities CAPABILITY_IAM \ + --no-confirm-changeset \ + --no-fail-on-empty-changeset -echo "All Lambda functions deployed successfully!" \ No newline at end of file +echo "Deployment completed successfully for $ENVIRONMENT environment!" \ No newline at end of file diff --git a/gogen-api/template.yaml b/gogen-api/template.yaml index d9507f9..d08a8ee 100644 --- a/gogen-api/template.yaml +++ b/gogen-api/template.yaml @@ -5,11 +5,54 @@ Resources: GoGenApi: Type: AWS::Serverless::Api Properties: - StageName: dev + StageName: v1 + OpenApiVersion: '2.0' + Auth: + DefaultAuthorizer: NONE + GatewayResponses: + DEFAULT_4XX: + ResponseParameters: + Headers: + Access-Control-Allow-Origin: !If + - IsProduction + - "'https://gogen.io'" + - "'https://staging.gogen.io'" + Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" + Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Requested-With'" + Access-Control-Allow-Credentials: "'true'" + DEFAULT_5XX: + ResponseParameters: + Headers: + Access-Control-Allow-Origin: !If + - IsProduction + - "'https://gogen.io'" + - "'https://staging.gogen.io'" + Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" + Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Requested-With'" + Access-Control-Allow-Credentials: "'true'" Cors: AllowMethods: "'GET,POST,OPTIONS'" - AllowHeaders: "'Content-Type,Authorization'" - AllowOrigin: "'*'" + AllowHeaders: "'Content-Type,Authorization,X-Requested-With'" + AllowOrigin: !If + - IsProduction + - "'https://gogen.io'" + - "'https://staging.gogen.io'" + AllowCredentials: true + MaxAge: 600 + MethodSettings: + - ResourcePath: '/*' + HttpMethod: '*' + CachingEnabled: false + ThrottlingBurstLimit: 5000 + ThrottlingRateLimit: 10000 + Domain: + DomainName: !If + - IsProduction + - api.gogen.io + - staging-api.gogen.io + CertificateArn: !Ref CertificateArn + EndpointConfiguration: REGIONAL + BasePath: '' GetFunction: Type: AWS::Serverless::Function @@ -21,6 +64,11 @@ Resources: CodeUri: ./api Handler: get.lambda_handler Runtime: python3.13 + Timeout: 10 + Role: !Ref LambdaRoleArn + Environment: + Variables: + ENVIRONMENT: !Ref Environment Events: GetGogen: Type: Api @@ -28,6 +76,12 @@ Resources: RestApiId: !Ref GoGenApi Path: /v1/get/{proxy+} Method: GET + GetGogenOptions: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/get/{proxy+} + Method: OPTIONS ListFunction: Type: AWS::Serverless::Function @@ -39,6 +93,11 @@ Resources: CodeUri: ./api Handler: list.lambda_handler Runtime: python3.13 + Timeout: 10 + Role: !Ref LambdaRoleArn + Environment: + Variables: + ENVIRONMENT: !Ref Environment Events: ListGogens: Type: Api @@ -46,6 +105,12 @@ Resources: RestApiId: !Ref GoGenApi Path: /v1/list Method: GET + ListGogensOptions: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/list + Method: OPTIONS SearchFunction: Type: AWS::Serverless::Function @@ -57,6 +122,11 @@ Resources: CodeUri: ./api Handler: search.lambda_handler Runtime: python3.13 + Timeout: 10 + Role: !Ref LambdaRoleArn + Environment: + Variables: + ENVIRONMENT: !Ref Environment Events: SearchGogens: Type: Api @@ -64,6 +134,12 @@ Resources: RestApiId: !Ref GoGenApi Path: /v1/search Method: GET + SearchGogensOptions: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/search + Method: OPTIONS UpsertFunction: Type: AWS::Serverless::Function @@ -75,6 +151,11 @@ Resources: CodeUri: ./api Handler: upsert.lambda_handler Runtime: python3.13 + Timeout: 10 + Role: !Ref LambdaRoleArn + Environment: + Variables: + ENVIRONMENT: !Ref Environment Events: UpsertGogen: Type: Api @@ -82,11 +163,17 @@ Resources: RestApiId: !Ref GoGenApi Path: /v1/upsert Method: POST + UpsertGogenOptions: + Type: Api + Properties: + RestApiId: !Ref GoGenApi + Path: /v1/upsert + Method: OPTIONS DynamoDBTable: Type: AWS::DynamoDB::Table Properties: - TableName: gogen + TableName: !Sub ${AWS::StackName}-gogen AttributeDefinitions: - AttributeName: gogen AttributeType: S @@ -97,7 +184,50 @@ Resources: ReadCapacityUnits: 5 WriteCapacityUnits: 5 + DynamoDBStagingTable: + Type: AWS::DynamoDB::Table + Condition: IsStagingEnvironment + Properties: + TableName: gogen-staging + AttributeDefinitions: + - AttributeName: gogen + AttributeType: S + KeySchema: + - AttributeName: gogen + KeyType: HASH + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 + +Conditions: + IsStagingEnvironment: !Equals + - !Ref Environment + - staging + IsProduction: !Equals + - !Ref Environment + - prod + +Parameters: + Environment: + Type: String + Default: prod + AllowedValues: + - staging + - prod + Description: The environment type + + CertificateArn: + Type: String + Description: ARN of the ACM certificate for *.gogen.io + + LambdaRoleArn: + Type: String + Description: ARN of the IAM role for Lambda functions + Outputs: ApiURL: Description: API Gateway endpoint URL - Value: !Sub "https://${GoGenApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" \ No newline at end of file + Value: !If + - IsProduction + - https://api.gogen.io/ + - https://staging-api.gogen.io/ \ No newline at end of file diff --git a/ui/.env b/ui/.env new file mode 100644 index 0000000..e01c8fb --- /dev/null +++ b/ui/.env @@ -0,0 +1 @@ +# API URL for the Gogen API\nVITE_API_URL=/api diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 0000000..e710af6 --- /dev/null +++ b/ui/.env.development @@ -0,0 +1 @@ +VITE_API_URL=/api # This will be proxied by Vite to localhost:4000 \ No newline at end of file diff --git a/ui/.env.production b/ui/.env.production new file mode 100644 index 0000000..7ce481b --- /dev/null +++ b/ui/.env.production @@ -0,0 +1 @@ +VITE_API_URL=https://api.gogen.io/v1 \ No newline at end of file diff --git a/ui/.env.staging b/ui/.env.staging new file mode 100644 index 0000000..6bdd7a8 --- /dev/null +++ b/ui/.env.staging @@ -0,0 +1 @@ +VITE_API_URL=https://staging-api.gogen.io/v1 \ No newline at end of file diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..709fb36 --- /dev/null +++ b/ui/README.md @@ -0,0 +1,117 @@ +# Gogen UI + +A modern React-based user interface for interacting with the Gogen API. This UI allows users to view, search, and execute Gogen configurations directly in the browser. + +## Features + +- View a list of all available Gogen configurations +- Search and filter configurations +- View detailed information about each configuration +- Execute configurations in the browser using WebAssembly +- Display execution results in terminal or structured format + +## Technology Stack + +- **Frontend Framework**: React with TypeScript +- **Styling**: Tailwind CSS +- **State Management**: React Context API and hooks +- **Routing**: React Router +- **API Client**: Axios +- **Terminal Emulation**: xterm.js +- **Build Tool**: Vite + +## Getting Started + +### Prerequisites + +- Node.js (v14 or later) +- npm (v6 or later) + +### Installation + +1. Clone the repository: + ```bash + git clone https://github.com/coccyx/gogen.git + cd gogen/ui + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Start the development server: + ```bash + npm run dev + ``` + +4. Open your browser and navigate to `http://localhost:3000` + +## Building for Production + +To build the application for production: + +```bash +npm run build +``` + +The built files will be in the `dist` directory. + +## Project Structure + +- `src/` - Source code + - `api/` - API client and interfaces + - `components/` - Reusable UI components + - `pages/` - Page components + - `hooks/` - Custom React hooks + - `utils/` - Utility functions + - `types/` - TypeScript type definitions + - `context/` - React context providers + - `assets/` - Static assets + +## Styling with Tailwind CSS + +This project uses Tailwind CSS for styling. We've extended the default Tailwind configuration with custom colors and component classes based on the Cribl design system. + +### Custom Colors + +The following custom colors are defined in `tailwind.config.js`: + +- `cribl-primary`: #261926 (Dark purple) +- `cribl-cyan`: #00F9BB (Bright teal/cyan) +- `cribl-purple`: #7f66ff (Bright purple) +- `cribl-pink`: #ff3399 (Bright pink) +- `cribl-red`: #f25e65 (Red) +- `cribl-white`: #ffffff (White) +- `cribl-gray`: #f1f1f1 (Light gray) +- `cribl-dark`: #1A1A1A (Dark background) +- `cribl-blue`: #3A85F7 (Blue) +- `cribl-orange`: #FF6B35 (Orange) +- `cribl-light-gray`: #F5F5F5 (Light gray background) + +### Custom Component Classes + +We've defined several reusable component classes in `src/index.css` using Tailwind's `@apply` directive: + +- `.btn-primary`: Primary button style using cribl-cyan +- `.btn-secondary`: Secondary button style using cribl-purple +- `.btn-outline`: Outline button style with cribl-cyan border +- `.card`: Card component with white background and shadow +- `.container-custom`: Container with responsive padding +- `.nav-link`: Navigation link with hover effect +- `.section-heading`: Section heading with consistent styling + +### Usage Example + +```jsx + +
+

Card Title

+

Card content

+
+Navigation Link +``` + +## License + +This project is licensed under the same license as the Gogen project. \ No newline at end of file diff --git a/ui/REQUIREMENTS.md b/ui/REQUIREMENTS.md new file mode 100644 index 0000000..e668d55 --- /dev/null +++ b/ui/REQUIREMENTS.md @@ -0,0 +1,88 @@ +# Gogen UI Requirements + +## Overview +The Gogen UI is a React-based web application that provides a user interface for interacting with the Gogen API. The UI allows users to view, search, and execute Gogen configurations. + +## Technical Stack +- **Frontend Framework**: React with TypeScript +- **Styling**: Tailwind CSS +- **State Management**: React Context API and hooks +- **Routing**: React Router +- **API Client**: Axios +- **Testing**: Jest and React Testing Library +- **Build Tool**: Vite + +## Color Scheme +The UI will follow Cribl.io's color scheme, which includes: +- Primary colors: Deep blues and purples (#261926, #7f66ff) +- Secondary colors: Bright accents (#ff3399, #f25e65) +- Neutral colors: White and light grays (#ffffff, #f1f1f1) + +## Features + +### 1. Configuration List View +- Display a list of all available Gogen configurations +- Show configuration name and description +- Allow sorting by name and description +- Provide a search box to filter configurations +- Include a button to execute a configuration + +### 2. Configuration Detail View +- Display the full details of a selected configuration +- Show the configuration in a pretty-printed format +- Provide a button to execute the configuration +- Include a "Back to List" button + +### 3. Configuration Execution +- Allow users to execute a configuration in the browser using the Gogen WASM module +- Provide options to configure the execution (based on available options in the configuration) +- Display execution results in one of two ways: + - Terminal-like output (using xterm.js) + - Structured event view for JSON output + +## Pages + +### 1. Home Page +- Welcome message and brief explanation of Gogen +- Quick links to the configuration list and other features + +### 2. Configurations List Page +- List of all configurations with search and filter capabilities +- Each configuration item shows name, description, and an "Execute" button +- Clicking on a configuration name navigates to the detail view + +### 3. Configuration Detail Page +- Detailed view of a single configuration +- Pretty-printed YAML/JSON display +- Execution options and controls + +### 4. Execution Page +- Controls for configuring and running the selected configuration +- Output display area (terminal or structured view) +- Options to switch between output display modes + +## Non-Functional Requirements + +### 1. Responsive Design +- The UI should be responsive and work well on mobile devices +- Layout should adapt to different screen sizes + +### 2. Performance +- Fast loading times for the configuration list +- Efficient rendering of large configurations +- Smooth execution of configurations in the browser + +### 3. Accessibility +- The UI should be accessible to users with disabilities +- Follow WCAG 2.1 AA guidelines + +### 4. Browser Compatibility +- Support modern browsers (Chrome, Firefox, Safari, Edge) +- Graceful degradation for older browsers + +## Future Enhancements (Not in Initial Scope) +- User authentication via GitHub +- Creating and editing configurations +- Saving execution results +- Sharing configurations and results +- Advanced filtering and sorting options \ No newline at end of file diff --git a/ui/SUMMARY.md b/ui/SUMMARY.md new file mode 100644 index 0000000..b291f68 --- /dev/null +++ b/ui/SUMMARY.md @@ -0,0 +1,82 @@ +# Gogen UI Project Summary + +## What We've Accomplished + +1. **Project Structure** + - Created a React project with TypeScript + - Set up the basic directory structure + - Created component and page files + - Set up routing with React Router + +2. **API Integration** + - Created an API client for interacting with the Gogen API + - Defined TypeScript interfaces for API responses + - Implemented functions for fetching configurations + - Updated ExecutionPage to use real API calls instead of mock data + - Tested API integration with the backend + +3. **UI Components** + - Created layout components (Navbar, Footer) + - Created page components (Home, Configurations, ConfigurationDetail, Execution) + - Implemented a terminal-like interface for execution results + +4. **Documentation** + - Created an OpenAPI specification for the Gogen API + - Created a detailed requirements document + - Created a README.md file + +5. **Styling and Configuration** + - Fixed Tailwind CSS configuration issues + - Implemented custom color scheme + - Created reusable component classes using Tailwind's @apply directive + - Implemented responsive design principles + +6. **UI Refinement** + - Removed Cribl branding and messaging + - Simplified the UI to focus on Gogen functionality + - Replaced hero section with Gogen-specific messaging + - Simplified navigation to only include Home for now + - Added a table to display Gogen configurations from the API + - Created configuration detail page with metadata and YAML display + - Fixed UI loading issues by simplifying the styling approach + +7. **WASM Integration** + - Created a WASM integration module for executing Gogen configurations in the browser + - Implemented error handling for WASM execution + - Removed non-existent API execution mode + - Added Go WASM runtime (wasm_exec.js) for proper WASM initialization + - Fixed WASM module loading and execution + - Implemented a virtual file system to pass configuration state to the WASM module + - Added command-line arguments support for the WASM module + - Enhanced error handling and user feedback for WASM execution + - Simplified WASM execution code for better maintainability + +## What Needs to Be Fixed + +1. **API Integration** + - ✅ Replaced mock data with real API data in the ExecutionPage component + - ✅ Added event count control for execution configuration + - ✅ Tested API integration with the backend + +2. **WASM Integration** + - ✅ Implemented the actual integration with the Gogen WASM module + - ✅ Removed references to non-existent API execution mode + - ✅ Fixed WASM initialization with proper Go runtime + - ✅ Implemented virtual file system for configuration state + - ✅ Added command-line arguments support for the WASM module + - ✅ Simplified WASM execution code for better maintainability + +3. **Testing** + - We need to implement unit tests for components + - We need to set up mocks for the API client + +## Next Steps + +1. ✅ Replace mock data with real API data +2. ✅ Test API integration with the backend +3. ✅ Implement the WASM integration +4. Add unit tests +5. Refine the UI components +6. Add more features (filtering, sorting, etc.) +7. Enhance the execution page with real-time updates +8. Implement error handling and loading states \ No newline at end of file diff --git a/ui/deploy_ui.sh b/ui/deploy_ui.sh new file mode 100755 index 0000000..6c30274 --- /dev/null +++ b/ui/deploy_ui.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -e + +# Determine script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Parse command line arguments +ENVIRONMENT="prod" # Default to production +while getopts "e:" opt; do + case $opt in + e) ENVIRONMENT="$OPTARG" + ;; + \?) echo "Invalid option -$OPTARG" >&2 + ;; + esac +done + +# Validate environment +if [[ "$ENVIRONMENT" != "prod" && "$ENVIRONMENT" != "staging" ]]; then + echo "Invalid environment: $ENVIRONMENT. Must be 'prod' or 'staging'" + exit 1 +fi + +# Ensure we're in the UI directory +cd "$SCRIPT_DIR" + +# Check if Node.js is installed +if ! command -v node &> /dev/null; then + echo "Node.js is not installed. Please install Node.js first." + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "npm is not installed. Please install npm first." + exit 1 +fi + +# Validate AWS credentials are configured +if ! aws sts get-caller-identity &> /dev/null; then + echo "AWS credentials are not configured. Please run 'aws configure' first." + exit 1 +fi + +# Install dependencies +echo "Installing dependencies..." +npm install + +# Build the application +echo "Building UI for $ENVIRONMENT environment..." +if [ "$ENVIRONMENT" = "prod" ]; then + npm run build + BUCKET="gogen.io" +else + npm run build:staging + BUCKET="staging.gogen.io" +fi + +# Deploy to S3 +echo "Deploying to s3://$BUCKET/" +aws s3 sync dist/ "s3://$BUCKET/" --delete + +echo "Deployment completed successfully for $ENVIRONMENT environment!" \ No newline at end of file diff --git a/ui/gogen-api-spec.yaml b/ui/gogen-api-spec.yaml new file mode 100644 index 0000000..0957c2b --- /dev/null +++ b/ui/gogen-api-spec.yaml @@ -0,0 +1,148 @@ +openapi: 3.0.0 +info: + title: Gogen API + description: API for interacting with Gogen configurations + version: 1.0.0 +servers: + - url: https://api.gogen.io/v1 + description: Production server +paths: + /list: + get: + summary: List all Gogen configurations + description: Returns a list of all available Gogen configurations with their descriptions + operationId: listConfigurations + responses: + '200': + description: A list of Gogen configurations + content: + application/json: + schema: + type: object + properties: + Items: + type: array + items: + $ref: '#/components/schemas/ConfigurationSummary' + /get/{configName}: + get: + summary: Get a specific Gogen configuration + description: Returns the details of a specific Gogen configuration + operationId: getConfiguration + parameters: + - name: configName + in: path + required: true + description: Name of the Gogen configuration to retrieve + schema: + type: string + responses: + '200': + description: A Gogen configuration + content: + application/json: + schema: + type: object + properties: + Item: + $ref: '#/components/schemas/Configuration' + '404': + description: Configuration not found + /search: + get: + summary: Search for Gogen configurations + description: Returns a list of Gogen configurations matching the search query + operationId: searchConfigurations + parameters: + - name: q + in: query + required: true + description: Search query + schema: + type: string + responses: + '200': + description: A list of matching Gogen configurations + content: + application/json: + schema: + type: object + properties: + Items: + type: array + items: + $ref: '#/components/schemas/ConfigurationSummary' + /upsert: + post: + summary: Create or update a Gogen configuration + description: Creates a new Gogen configuration or updates an existing one + operationId: upsertConfiguration + security: + - githubAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Configuration' + responses: + '200': + description: Configuration created or updated successfully + '400': + description: Invalid request +components: + schemas: + ConfigurationSummary: + type: object + properties: + gogen: + type: string + description: The name of the Gogen configuration + description: + type: string + description: A brief description of the configuration + Configuration: + type: object + properties: + gogen: + type: string + description: The name of the Gogen configuration + description: + type: string + description: A brief description of the configuration + config: + type: string + description: The configuration content + samples: + type: array + description: Sample configurations + items: + type: object + raters: + type: array + description: Rate configurations + items: + type: object + mix: + type: array + description: Mix configurations + items: + type: object + generators: + type: array + description: Generator configurations + items: + type: object + global: + type: object + description: Global configuration settings + templates: + type: array + description: Template configurations + items: + type: object + securitySchemes: + githubAuth: + type: http + scheme: bearer + description: GitHub authentication token \ No newline at end of file diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..44ef93d --- /dev/null +++ b/ui/index.html @@ -0,0 +1,15 @@ + + + + + + + + Gogen UI + + + +
+ + + \ No newline at end of file diff --git a/ui/jest.config.ts b/ui/jest.config.ts new file mode 100644 index 0000000..a711efb --- /dev/null +++ b/ui/jest.config.ts @@ -0,0 +1,33 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['/src/setupTests.ts'], + moduleNameMapper: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + '^@/(.*)$': '/src/$1', + }, + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, + testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/main.tsx', + '!src/vite-env.d.ts', + ], + coverageThreshold: { + global: { + branches: 70, + functions: 70, + lines: 70, + statements: 70, + }, + }, +}; + +export default config; \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..5e0afd5 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,8457 @@ +{ + "name": "gogen-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gogen-ui", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "axios": "^1.6.7", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.1", + "typescript": "^5.3.3", + "xterm": "^5.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@testing-library/user-event": "^14.6.1", + "@types/jest": "^29.5.12", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.20", + "canvas": "^2.11.2", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "ts-jest": "^29.2.6", + "ts-node": "^10.9.2", + "vite": "^5.1.3" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "dev": true + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz", + "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", + "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", + "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", + "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", + "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", + "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", + "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", + "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", + "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", + "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", + "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", + "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" + }, + "node_modules/@types/react": { + "version": "18.3.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001701", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", + "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "dev": true + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.109", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.109.tgz", + "integrity": "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", + "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.1", + "lightningcss-darwin-x64": "1.29.1", + "lightningcss-freebsd-x64": "1.29.1", + "lightningcss-linux-arm-gnueabihf": "1.29.1", + "lightningcss-linux-arm64-gnu": "1.29.1", + "lightningcss-linux-arm64-musl": "1.29.1", + "lightningcss-linux-x64-gnu": "1.29.1", + "lightningcss-linux-x64-musl": "1.29.1", + "lightningcss-win32-arm64-msvc": "1.29.1", + "lightningcss-win32-x64-msvc": "1.29.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", + "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", + "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", + "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", + "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", + "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", + "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", + "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", + "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", + "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", + "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "peer": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz", + "integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", + "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.9", + "@rollup/rollup-android-arm64": "4.34.9", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-freebsd-arm64": "4.34.9", + "@rollup/rollup-freebsd-x64": "4.34.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", + "@rollup/rollup-linux-arm-musleabihf": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", + "@rollup/rollup-linux-riscv64-gnu": "4.34.9", + "@rollup/rollup-linux-s390x-gnu": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-ia32-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dev": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/ts-jest": { + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vite": { + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..b62b1a4 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,51 @@ +{ + "name": "gogen-ui", + "version": "1.0.0", + "description": "UI for interacting with Gogen API", + "type": "module", + "scripts": { + "copy-wasm": "cp ../build/wasm/gogen.wasm public/", + "dev": "npm run copy-wasm && vite", + "build": "npm run copy-wasm && tsc && vite build", + "build:staging": "npm run copy-wasm && tsc && vite build --mode staging", + "preview": "vite preview", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" + }, + "keywords": [ + "gogen", + "react", + "ui" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "axios": "^1.6.7", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.1", + "typescript": "^5.3.3", + "xterm": "^5.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@testing-library/user-event": "^14.6.1", + "@types/jest": "^29.5.12", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.20", + "canvas": "^2.11.2", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "ts-jest": "^29.2.6", + "ts-node": "^10.9.2", + "vite": "^5.1.3" + } +} diff --git a/ui/postcss.config.js b/ui/postcss.config.js new file mode 100644 index 0000000..387612e --- /dev/null +++ b/ui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 0000000..cb09d66 --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,4 @@ + + + G + \ No newline at end of file diff --git a/ui/public/wasm_exec.js b/ui/public/wasm_exec.js new file mode 100644 index 0000000..d71af9e --- /dev/null +++ b/ui/public/wasm_exec.js @@ -0,0 +1,575 @@ +// Copyright 2018 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. + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.path) { + globalThis.path = { + resolve(...pathSegments) { + return pathSegments.join("/"); + } + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const testCallExport = (a, b) => { + this._inst.exports.testExport0(); + return this._inst.exports.testExport(a, b); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + callExport: testCallExport, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})(); diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx new file mode 100644 index 0000000..c66d40d --- /dev/null +++ b/ui/src/App.test.tsx @@ -0,0 +1,69 @@ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import App from './App'; +import '@testing-library/jest-dom'; + +// Mock the child components +jest.mock('./components/Layout', () => { + return { + __esModule: true, + default: ({ children }: { children: React.ReactNode }) =>
{children}
+ }; +}); + +jest.mock('./pages/HomePage', () => { + return { + __esModule: true, + default: () =>
Home Page
+ }; +}); + +jest.mock('./pages/ConfigurationDetailPage', () => { + return { + __esModule: true, + default: () =>
Configuration Detail Page
+ }; +}); + +jest.mock('./pages/NotFoundPage', () => { + return { + __esModule: true, + default: () =>
404 Page Not Found
+ }; +}); + +// Mock the BrowserRouter component from react-router-dom +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + BrowserRouter: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +describe('App Component', () => { + const renderWithRouter = (initialEntries = ['/', '/configurations/owner/config-name']) => { + return render( + + + + ); + }; + + it('renders the layout component', () => { + renderWithRouter(); + expect(screen.getByTestId('mock-layout')).toBeInTheDocument(); + }); + + it('renders home page on root path', () => { + renderWithRouter(['/']); + expect(screen.getByTestId('mock-home-page')).toBeInTheDocument(); + }); + + it('renders configuration detail page on configuration path', () => { + renderWithRouter(['/configurations/owner/config-name']); + expect(screen.getByTestId('mock-config-detail-page')).toBeInTheDocument(); + }); + + it('renders not found page for unknown routes', () => { + renderWithRouter(['/unknown-route']); + expect(screen.getByTestId('mock-not-found-page')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..a73daaf --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,21 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import Layout from './components/Layout'; +import HomePage from './pages/HomePage'; +import ConfigurationDetailPage from './pages/ConfigurationDetailPage'; +import NotFoundPage from './pages/NotFoundPage'; + +function App() { + return ( + + + + } /> + } /> + } /> + + + + ); +} + +export default App; \ No newline at end of file diff --git a/ui/src/api/__mocks__/api.ts b/ui/src/api/__mocks__/api.ts new file mode 100644 index 0000000..dce06cb --- /dev/null +++ b/ui/src/api/__mocks__/api.ts @@ -0,0 +1,36 @@ +import { Configuration, ConfigurationSummary } from '../gogenApi'; + +export const mockConfigurations: ConfigurationSummary[] = [ + { + gogen: 'Test Config 1', + description: 'A test configuration', + }, + { + gogen: 'Test Config 2', + description: 'Another test configuration', + }, +]; + +export const mockConfigurationDetails: Configuration[] = mockConfigurations.map(config => ({ + ...config, + config: 'config: test', + samples: [], + raters: [], + mix: [], + generators: [], + global: {}, + templates: [], +})); + +export const api = { + listConfigurations: jest.fn().mockResolvedValue(mockConfigurations), + getConfiguration: jest.fn().mockImplementation((name: string) => + Promise.resolve(mockConfigurationDetails.find(c => c.gogen === name)) + ), + searchConfigurations: jest.fn().mockImplementation((query: string) => + Promise.resolve(mockConfigurations.filter(c => + c.gogen.toLowerCase().includes(query.toLowerCase()) || + c.description.toLowerCase().includes(query.toLowerCase()) + )) + ), +}; \ No newline at end of file diff --git a/ui/src/api/gogenApi.test.ts b/ui/src/api/gogenApi.test.ts new file mode 100644 index 0000000..6b81db4 --- /dev/null +++ b/ui/src/api/gogenApi.test.ts @@ -0,0 +1,137 @@ +import axios, { AxiosInstance } from 'axios'; + +// Mock axios before importing gogenApi +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +// Create mock instance before importing gogenApi +const mockAxiosInstance = { + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + defaults: { + baseURL: '/api', + headers: { + 'Content-Type': 'application/json' + } + }, + interceptors: { + request: { use: jest.fn(), eject: jest.fn(), clear: jest.fn() }, + response: { use: jest.fn(), eject: jest.fn(), clear: jest.fn() } + } +} as unknown as jest.Mocked; + +// Set up axios.create mock before importing gogenApi +mockedAxios.create.mockReturnValue(mockAxiosInstance); + +// Import gogenApi after mock setup +import gogenApi, { ConfigurationSummary, Configuration } from './gogenApi'; + +describe('gogenApi', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Reset all mock methods + mockAxiosInstance.get.mockReset(); + mockAxiosInstance.post.mockReset(); + mockAxiosInstance.put.mockReset(); + mockAxiosInstance.delete.mockReset(); + // Refresh axios.create mock + mockedAxios.create.mockReturnValue(mockAxiosInstance); + }); + + describe('listConfigurations', () => { + it('should fetch and return a list of configurations', async () => { + const mockConfigs: ConfigurationSummary[] = [ + { gogen: 'test1', description: 'Test Config 1' }, + { gogen: 'test2', description: 'Test Config 2' }, + ]; + + mockAxiosInstance.get.mockResolvedValueOnce({ data: { Items: mockConfigs } }); + + const result = await gogenApi.listConfigurations(); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith('/list'); + expect(result).toEqual(mockConfigs); + }); + + it('should handle empty response', async () => { + mockAxiosInstance.get.mockResolvedValueOnce({ data: { Items: [] } }); + + const result = await gogenApi.listConfigurations(); + + expect(result).toEqual([]); + }); + + it('should handle error', async () => { + const error = new Error('Network error'); + mockAxiosInstance.get.mockRejectedValueOnce(error); + + await expect(gogenApi.listConfigurations()).rejects.toThrow('Network error'); + }); + }); + + describe('getConfiguration', () => { + it('should fetch and return a specific configuration', async () => { + const mockConfig: Configuration = { + gogen: 'test1', + description: 'Test Config 1', + config: 'test config content', + }; + + mockAxiosInstance.get.mockResolvedValueOnce({ data: { Item: mockConfig } }); + + const result = await gogenApi.getConfiguration('test1'); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith('/get/test1'); + expect(result).toEqual(mockConfig); + }); + + it('should handle empty response', async () => { + mockAxiosInstance.get.mockResolvedValueOnce({ data: { Item: {} } }); + + const result = await gogenApi.getConfiguration('test1'); + + expect(result).toEqual({}); + }); + + it('should handle error', async () => { + const error = new Error('Configuration not found'); + mockAxiosInstance.get.mockRejectedValueOnce(error); + + await expect(gogenApi.getConfiguration('test1')).rejects.toThrow('Configuration not found'); + }); + }); + + describe('searchConfigurations', () => { + it('should search and return matching configurations', async () => { + const mockConfigs: ConfigurationSummary[] = [ + { gogen: 'test1', description: 'Test Config 1' }, + ]; + + mockAxiosInstance.get.mockResolvedValueOnce({ data: { Items: mockConfigs } }); + + const result = await gogenApi.searchConfigurations('test'); + + expect(mockAxiosInstance.get).toHaveBeenCalledWith('/search', { + params: { q: 'test' }, + }); + expect(result).toEqual(mockConfigs); + }); + + it('should handle empty search results', async () => { + mockAxiosInstance.get.mockResolvedValueOnce({ data: { Items: [] } }); + + const result = await gogenApi.searchConfigurations('nonexistent'); + + expect(result).toEqual([]); + }); + + it('should handle error', async () => { + const error = new Error('Search failed'); + mockAxiosInstance.get.mockRejectedValueOnce(error); + + await expect(gogenApi.searchConfigurations('test')).rejects.toThrow('Search failed'); + }); + }); +}); \ No newline at end of file diff --git a/ui/src/api/gogenApi.ts b/ui/src/api/gogenApi.ts new file mode 100644 index 0000000..1a63d6d --- /dev/null +++ b/ui/src/api/gogenApi.ts @@ -0,0 +1,66 @@ +import axios from 'axios'; +import { config } from '../config'; + +// Create an axios instance with default config +const apiClient = axios.create({ + baseURL: config.apiBaseUrl, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Define interfaces for API responses +export interface ConfigurationSummary { + gogen: string; + description: string; +} + +export interface Configuration extends ConfigurationSummary { + config?: string; + samples?: any[]; + raters?: any[]; + mix?: any[]; + generators?: any[]; + global?: any; + templates?: any[]; +} + +// API functions +export const gogenApi = { + // Get a list of all configurations + listConfigurations: async (): Promise => { + try { + const response = await apiClient.get('/list'); + return response.data.Items || []; + } catch (error) { + console.error('Error fetching configurations:', error); + throw error; + } + }, + + // Get a specific configuration by name + getConfiguration: async (configName: string): Promise => { + try { + const response = await apiClient.get(`/get/${configName}`); + return response.data.Item || {}; + } catch (error) { + console.error(`Error fetching configuration ${configName}:`, error); + throw error; + } + }, + + // Search for configurations + searchConfigurations: async (query: string): Promise => { + try { + const response = await apiClient.get('/search', { + params: { q: query }, + }); + return response.data.Items || []; + } catch (error) { + console.error('Error searching configurations:', error); + throw error; + } + }, +}; + +export default gogenApi; \ No newline at end of file diff --git a/ui/src/api/gogenWasm.test.ts b/ui/src/api/gogenWasm.test.ts new file mode 100644 index 0000000..3e48791 --- /dev/null +++ b/ui/src/api/gogenWasm.test.ts @@ -0,0 +1,366 @@ +import { executeConfiguration, ExecutionParams } from './gogenWasm'; +import { Configuration } from './gogenApi'; + +// Get the global object +const global = globalThis as any; + +// Define Go instance interface +interface MockGoInstance { + argv: string[]; + env: Record; + importObject: { + env: Record; + go: Record; + }; + run: jest.Mock; + exit: jest.Mock; +} + +// Mock Go class with static instance tracking +class MockGo implements MockGoInstance { + static lastInstance: MockGo | null = null; + + // Instance properties + argv: string[] = []; + env: Record = {}; + importObject: { + env: Record; + go: Record; + } = { + env: { + 'runtime.ticks': jest.fn(() => 0), + 'runtime.sleepTicks': jest.fn(), + 'syscall/js.valueGet': jest.fn(), + 'syscall/js.valueSet': jest.fn(), + 'syscall/js.valueIndex': jest.fn(), + 'syscall/js.valueCall': jest.fn(), + 'syscall/js.valueNew': jest.fn(), + 'syscall/js.valueLength': jest.fn(), + 'syscall/js.valuePrepareString': jest.fn(), + 'syscall/js.valueLoadString': jest.fn(), + 'syscall/js.finalizeRef': jest.fn(), + }, + go: { + 'runtime.wasmExit': jest.fn(), + 'runtime.wasmWrite': jest.fn((sp: number) => { + // Mock writing to stdout + const instance = MockGo.getInstance(); + if (instance && global.fs) { + const fd = Number(instance.mem.getBigInt64(sp + 8)); + const p = Number(instance.mem.getBigInt64(sp + 16)); + const n = instance.mem.getInt32(sp + 24); + const buf = new Uint8Array(instance.mem.buffer, p, n); + global.fs.writeSync(fd, buf); + } + }), + 'runtime.resetMemoryDataView': jest.fn(), + 'runtime.nanotime1': jest.fn(() => 0), + 'runtime.walltime1': jest.fn(() => 0), + 'runtime.scheduleTimeoutEvent': jest.fn(), + 'runtime.clearTimeoutEvent': jest.fn(), + 'runtime.getRandomData': jest.fn(), + }, + }; + run: jest.Mock = jest.fn().mockResolvedValue(undefined); + exit: jest.Mock = jest.fn(); + mem: DataView; + + constructor() { + // Set up prototype chain + Object.setPrototypeOf(this, MockGo.prototype); + + // Initialize instance properties + this.argv = []; + this.env = {}; + this.mem = new DataView(new ArrayBuffer(1024 * 1024)); // 1MB buffer for testing + + // Track instance + MockGo.lastInstance = this; + } + + static clearInstance() { + MockGo.lastInstance = null; + } + + static getInstance(): MockGo { + if (!MockGo.lastInstance) { + throw new Error('No MockGo instance available'); + } + return MockGo.lastInstance; + } +} + +// Set up constructor properties +Object.defineProperty(MockGo, 'prototype', { + writable: false, + enumerable: false, + configurable: false, +}); + +// Mock WebAssembly +const mockWebAssembly = { + instantiate: jest.fn().mockResolvedValue({ + instance: { exports: {} }, + module: {}, + }) +}; + +// Mock TextDecoder and TextEncoder +const mockTextDecoder = { + decode: jest.fn((buf) => Buffer.from(buf).toString()) +}; + +const mockTextEncoder = { + encode: jest.fn((str) => Buffer.from(str)) +}; + +describe('gogenWasm', () => { + let originalWindow: any; + let originalWebAssembly: any; + let originalTextDecoder: any; + let originalTextEncoder: any; + let mockFetch: jest.Mock; + + beforeEach(() => { + // Store original globals + originalWindow = global.window; + originalWebAssembly = global.WebAssembly; + originalTextDecoder = global.TextDecoder; + originalTextEncoder = global.TextEncoder; + + // Create window if it doesn't exist + if (!global.window) { + global.window = {}; + } + + // Directly assign Go constructor + global.window.Go = MockGo; + + // Debug logging + console.log('Mock setup:', { + 'window.Go exists': !!global.window.Go, + 'window.Go is constructor': typeof global.window.Go === 'function', + 'window.Go prototype': Object.getPrototypeOf(global.window.Go), + 'MockGo is constructor': typeof MockGo === 'function', + 'MockGo prototype': Object.getPrototypeOf(MockGo), + }); + + // Mock WebAssembly + global.WebAssembly = mockWebAssembly as any; + + // Mock TextDecoder and TextEncoder + global.TextDecoder = jest.fn(() => mockTextDecoder) as any; + global.TextEncoder = jest.fn(() => mockTextEncoder) as any; + + // Mock fetch + mockFetch = jest.fn(() => + Promise.resolve({ + arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)) + }) + ); + global.fetch = mockFetch; + + // Reset all mocks and static instance + jest.clearAllMocks(); + MockGo.clearInstance(); + }); + + afterEach(() => { + // Restore original globals + global.window = originalWindow; + global.WebAssembly = originalWebAssembly; + global.TextDecoder = originalTextDecoder; + global.TextEncoder = originalTextEncoder; + + // Clean up virtual filesystem and static instance + if (global.fs) { + delete global.fs; + } + MockGo.clearInstance(); + }); + + describe('executeConfiguration', () => { + let mockConfig: Configuration; + let mockParams: ExecutionParams; + let outputLines: string[] = []; + let onOutput: jest.Mock; + let mockGoInstance: MockGo; + let outputBuffer: { stdout: string; stderr: string }; + + beforeEach(() => { + outputLines = []; + onOutput = jest.fn(); + outputBuffer = { + stdout: '', + stderr: '' + }; + + mockConfig = { + gogen: 'test', + description: 'Test configuration', + config: 'test: true' + }; + + mockParams = { + eventCount: 10, + intervals: 5, + intervalSeconds: 1, + outputTemplate: 'json' as const + }; + + // Set up virtual filesystem with proper file descriptors + global.fs = { + constants: { + O_WRONLY: 1, + O_RDWR: 2, + O_CREAT: 64, + O_TRUNC: 512, + O_APPEND: 1024, + O_EXCL: 128, + }, + writeSync: jest.fn((fd: number, buf: Uint8Array) => { + const text = new TextDecoder().decode(buf); + + // Accumulate output in buffer + if (fd === 1) { + outputBuffer.stdout += text; + const lines = outputBuffer.stdout.split('\n'); + if (lines.length > 1) { + // Process all complete lines + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i]; + if (line.length > 0) { + outputLines.push(line); + onOutput(line); + } + } + // Keep the partial line + outputBuffer.stdout = lines[lines.length - 1]; + } + } else if (fd === 2) { + outputBuffer.stderr += text; + const lines = outputBuffer.stderr.split('\n'); + if (lines.length > 1) { + // Process all complete lines + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i]; + if (line.length > 0) { + onOutput(`ERROR: ${line}`); + } + } + // Keep the partial line + outputBuffer.stderr = lines[lines.length - 1]; + } + } + return buf.length; + }), + readSync: jest.fn(), + read: jest.fn(), + open: jest.fn((_path, _flags, _mode, callback) => callback(null, 3)), // Start at fd 3 for files + stat: jest.fn((_path, callback) => callback(null, { isFile: () => true, size: 100 })), + }; + + // Ensure window.Go is set and create instance + if (!global.window) { + global.window = {}; + } + mockGoInstance = new MockGo(); + global.window.Go = jest.fn(() => mockGoInstance); + }); + + // Test output handling separately from WASM execution + describe('output handling', () => { + it('should handle stdout output correctly', () => { + // Write test output + const testOutput = 'test output\n'; + const outputBuffer = Buffer.from(testOutput); + global.fs.writeSync(1, outputBuffer); + + expect(outputLines).toContain('test output'); + expect(onOutput).toHaveBeenCalledWith('test output'); + }); + + it('should handle stderr output correctly', () => { + // Write error output + const errorOutput = 'error message\n'; + const errorBuffer = Buffer.from(errorOutput); + global.fs.writeSync(2, errorBuffer); + + expect(onOutput).toHaveBeenCalledWith('ERROR: error message'); + }); + }); + + // Original tests for executeConfiguration + it('should verify Go constructor is available', () => { + expect(global.window.Go).toBeDefined(); + expect(typeof global.window.Go).toBe('function'); + expect(() => new global.window.Go()).not.toThrow(); + expect(new global.window.Go()).toBe(mockGoInstance); + }); + + it('should handle Go execution errors', async () => { + mockGoInstance.run.mockRejectedValue(new Error('Execution failed')); + await expect(executeConfiguration(mockConfig, mockParams, onOutput)).rejects.toThrow('Execution failed'); + }); + + it('should clean up virtual filesystem after execution', async () => { + const originalFs = { ...global.fs }; + mockGoInstance.run.mockRejectedValue(new Error('Execution failed')); + + try { + await executeConfiguration(mockConfig, mockParams, onOutput); + } catch (error) { + // Expected error + } + + expect(global.fs).toEqual(originalFs); + }); + + it('should handle partial output lines correctly', async () => { + // Write partial output + const partialOutput = 'partial output'; + const outputBuffer = Buffer.from(partialOutput); // No newline + global.fs.writeSync(1, outputBuffer); + + expect(outputLines).not.toContain(partialOutput); // Should not process without newline + expect(onOutput).not.toHaveBeenCalled(); + }); + + it('should pass correct arguments to Go instance', async () => { + try { + await executeConfiguration(mockConfig, mockParams, onOutput); + } catch (error) { + // Expected error + } + + expect(mockGoInstance.argv).toEqual([ + 'gogen', + '-c', + '/config.yml', + '-ot', + 'json', + 'gen', + '-ei', + '5', + '-i', + '1', + '-c', + '10' + ]); + }); + + it('should handle different output templates', async () => { + const configuredParams = { + ...mockParams, + outputTemplate: 'configured' as const + }; + try { + await executeConfiguration(mockConfig, configuredParams, onOutput); + } catch (error) { + // Expected error + } + + expect(mockGoInstance.argv).not.toContain('-ot'); + }); + }); +}); \ No newline at end of file diff --git a/ui/src/api/gogenWasm.ts b/ui/src/api/gogenWasm.ts new file mode 100644 index 0000000..bdd059c --- /dev/null +++ b/ui/src/api/gogenWasm.ts @@ -0,0 +1,270 @@ +import { Configuration } from './gogenApi'; + +export interface ExecutionParams { + intervals: number; + intervalSeconds: number; + eventCount: number; + outputTemplate: 'raw' | 'json' | 'configured'; +} + +// Define the Go type from wasm_exec.js +declare global { + interface Window { + Go: any; + } +} + +interface VirtualFsSetup { + output: string[]; + outputBuf: string; + decoder: TextDecoder; + originalFs: { + writeSync: any; + stat: any; + open: any; + read: any; + readSync: any; + }; + cleanup: () => void; +} + +const setupVirtualFileSystem = ( + configuration: Configuration, + onOutput?: (line: string) => void +): VirtualFsSetup => { + const output: string[] = []; + const decoder = new TextDecoder(); + let outputBuf = ""; + + // Set up virtual filesystem for config + const virtualFiles: { [key: string]: string } = { + '/config.yml': configuration.config || '' + }; + + // Track file positions for virtual files + const virtualFilePositions: { [key: number]: number } = {}; + + // Create a complete stats object that matches Node.js fs.Stats + const createStats = (isFile: boolean, size: number = 0) => ({ + isFile: () => isFile, + isDirectory: () => !isFile, + isBlockDevice: () => false, + isCharacterDevice: () => false, + isSymbolicLink: () => false, + isFIFO: () => false, + isSocket: () => false, + dev: 0, + ino: 0, + mode: isFile ? 0o666 : 0o777, + nlink: 1, + uid: 0, + gid: 0, + rdev: 0, + size, + blksize: 4096, + blocks: Math.ceil(size / 4096), + atimeMs: Date.now(), + mtimeMs: Date.now(), + ctimeMs: Date.now(), + birthtimeMs: Date.now(), + atime: new Date(), + mtime: new Date(), + ctime: new Date(), + birthtime: new Date() + }); + + const global = globalThis as any; + if (!global.fs) { + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 } + }; + } + + // Store original methods + const originalFs = { + writeSync: global.fs.writeSync, + stat: global.fs.stat, + open: global.fs.open, + read: global.fs.read, + readSync: global.fs.readSync + }; + + // Set up the fs methods + let stdoutBuf = ''; + let stderrBuf = ''; + + global.fs.writeSync = (fd: number, buf: Uint8Array) => { + // fd 1 is stdout, fd 2 is stderr + const line = decoder.decode(buf); + + if (fd === 1) { + stdoutBuf = processOutput(stdoutBuf, line, (line) => { + output.push(line); + if (onOutput) { + onOutput(line); + } + console.log(line); + }); + } else if (fd === 2) { + stderrBuf = processOutput(stderrBuf, line, (line) => { + console.error(line); + if (onOutput) { + onOutput(`ERROR: ${line}`); + } + }); + } + + return buf.length; + }; + + // Helper function to process output buffers + const processOutput = ( + buffer: string, + newData: string, + lineHandler: (line: string) => void + ): string => { + buffer += newData; + const nl = buffer.lastIndexOf("\n"); + if (nl !== -1) { + const line = buffer.substring(0, nl); + lineHandler(line); + return buffer.substring(nl + 1); + } + return buffer; + }; + + global.fs.stat = (path: string, callback: Function) => { + console.log('stat', path); + if (path in virtualFiles) { + callback(null, createStats(true, virtualFiles[path].length)); + } else if (path === '/') { + callback(null, createStats(false)); + } else if (typeof originalFs.stat === 'function') { + originalFs.stat(path, callback); + } else { + callback(new Error(`ENOENT: no such file or directory, stat '${path}'`)); + } + }; + + global.fs.open = (path: string, flags: string, mode: number, callback: Function) => { + console.log('open', path, flags, mode); + if (path in virtualFiles) { + const fd = 3; + virtualFilePositions[fd] = 0; + callback(null, fd); + } else if (typeof originalFs.open === 'function') { + originalFs.open(path, flags, mode, callback); + } else { + callback(new Error(`ENOENT: no such file or directory, open '${path}'`)); + } + }; + + global.fs.read = (fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, callback: Function) => { + console.log('read', fd, offset, length, position); + if (fd === 3 && '/config.yml' in virtualFiles) { + const content = virtualFiles['/config.yml']; + let pos = position !== null ? position : (virtualFilePositions[fd] || 0); + + if (pos >= content.length) { + callback(null, 0); + return; + } + + const data = new TextEncoder().encode(content).slice(pos, pos + length); + buffer.set(data, offset); + + if (position === null) { + virtualFilePositions[fd] = pos + data.length; + } + + callback(null, data.length); + } else if (typeof originalFs.read === 'function') { + originalFs.read(fd, buffer, offset, length, position, callback); + } else { + callback(new Error('EBADF: bad file descriptor')); + } + }; + + const cleanup = () => { + // Restore original fs methods + Object.assign(global.fs, originalFs); + }; + + return { + output, + outputBuf, + decoder, + originalFs, + cleanup + }; +}; + +/** + * Execute a Gogen configuration using WebAssembly + * + * @param configuration The configuration to execute + * @param params The execution parameters + * @param onOutput Optional callback for streaming output + * @returns Array of output lines + */ +export const executeConfiguration = async ( + configuration: Configuration, + params: ExecutionParams, + onOutput?: (line: string) => void +): Promise => { + const fsSetup = setupVirtualFileSystem(configuration, onOutput); + + try { + // Initialize a new Go instance for each execution + const go = new window.Go(); + + // Fetch and instantiate a new WASM module + const wasmResponse = await fetch('/gogen.wasm'); + const wasmBytes = await wasmResponse.arrayBuffer(); + const wasmResult = await WebAssembly.instantiate(wasmBytes, go.importObject); + + // Build command line arguments + const args = ['gogen', '-c', '/config.yml']; + + // Add output template (-ot) if not 'configured' + if (params.outputTemplate !== 'configured') { + args.push('-ot', params.outputTemplate); + } + + // Add gen command and remaining arguments + args.push('gen'); + + // Add event interval count (-ei) + args.push('-ei', params.intervals.toString()); + + // Add interval seconds (-i) + args.push('-i', params.intervalSeconds.toString()); + + // Add event count (-c) + args.push('-c', params.eventCount.toString()); + + // Set up arguments and run + go.argv = args; + + try { + await go.run(wasmResult.instance); + return fsSetup.output; + } catch (error) { + // Ensure any partial output is flushed + if (fsSetup.outputBuf && onOutput) { + onOutput(fsSetup.outputBuf); + } + throw error; + } + } catch (error: any) { + console.error('Error executing WASM:', error); + throw error; + } finally { + // Always clean up the virtual filesystem + fsSetup.cleanup(); + } +}; + +export default { + executeConfiguration, +}; \ No newline at end of file diff --git a/ui/src/components/ConfigurationList.test.tsx b/ui/src/components/ConfigurationList.test.tsx new file mode 100644 index 0000000..f386ce2 --- /dev/null +++ b/ui/src/components/ConfigurationList.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from '../utils/test-utils'; +import { MemoryRouter } from 'react-router-dom'; +import ConfigurationList from './ConfigurationList'; +import { ConfigurationSummary } from '../api/gogenApi'; + +const mockConfigurations: ConfigurationSummary[] = [ + { + gogen: 'test-config', + description: 'Test Configuration', + }, + { + gogen: 'another-config', + description: 'Another Configuration', + }, +]; + +const renderWithRouter = (props: { + configurations: ConfigurationSummary[]; + loading: boolean; + error: string | null; +}) => { + return render( + + + + ); +}; + +describe('ConfigurationList', () => { + it('shows loading state', () => { + renderWithRouter({ configurations: [], loading: true, error: null }); + expect(screen.getByRole('status')).toBeInTheDocument(); + }); + + it('shows error message', () => { + const errorMessage = 'Failed to load configurations'; + renderWithRouter({ configurations: [], loading: false, error: errorMessage }); + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + + it('renders list of configurations', () => { + renderWithRouter({ configurations: mockConfigurations, loading: false, error: null }); + + mockConfigurations.forEach((config) => { + expect(screen.getByText(config.gogen)).toBeInTheDocument(); + expect(screen.getByText(config.description)).toBeInTheDocument(); + }); + }); + + it('handles empty configurations list', () => { + renderWithRouter({ configurations: [], loading: false, error: null }); + expect(screen.getByText('No configurations found matching your search.')).toBeInTheDocument(); + }); + + it('has correct styling classes', () => { + renderWithRouter({ configurations: mockConfigurations, loading: false, error: null }); + + // Check table styling + const table = screen.getByRole('table'); + expect(table).toHaveClass('min-w-full'); + + // Check header styling + const headers = screen.getAllByRole('columnheader'); + headers.forEach(header => { + expect(header).toHaveClass('px-6', 'py-3', 'text-left', 'text-xs', 'font-medium', 'text-gray-500', 'uppercase', 'tracking-wider'); + }); + + // Check table container styling + const tableContainer = screen.getByRole('table').closest('div'); + expect(tableContainer).toHaveClass('bg-white', 'rounded-lg', 'shadow', 'overflow-hidden'); + }); +}); \ No newline at end of file diff --git a/ui/src/components/ConfigurationList.tsx b/ui/src/components/ConfigurationList.tsx new file mode 100644 index 0000000..8ab1815 --- /dev/null +++ b/ui/src/components/ConfigurationList.tsx @@ -0,0 +1,148 @@ +import { useState, useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { ConfigurationSummary } from '../api/gogenApi'; +import LoadingSpinner from './LoadingSpinner'; + +interface ConfigurationListProps { + configurations: ConfigurationSummary[]; + loading: boolean; + error: string | null; +} + +const ConfigurationList = ({ configurations, loading, error }: ConfigurationListProps) => { + const [currentPage, setCurrentPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(''); + const itemsPerPage = 10; + + // Filter and sort configurations + const filteredAndSortedConfigs = useMemo(() => { + return configurations + .filter(config => + config.gogen.toLowerCase().includes(searchQuery.toLowerCase()) || + (config.description || '').toLowerCase().includes(searchQuery.toLowerCase()) + ) + .sort((a, b) => a.gogen.localeCompare(b.gogen)); + }, [configurations, searchQuery]); + + // Calculate pagination + const totalPages = Math.ceil(filteredAndSortedConfigs.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const paginatedConfigs = filteredAndSortedConfigs.slice(startIndex, startIndex + itemsPerPage); + + // Handle page changes + const handlePageChange = (page: number) => { + setCurrentPage(page); + window.scrollTo(0, 0); + }; + + if (loading) return ; + if (error) return
{error}
; + + return ( +
+ {/* Search Filter */} +
+ { + setSearchQuery(e.target.value); + setCurrentPage(1); // Reset to first page when searching + }} + className="w-full px-4 py-2 rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white text-gray-900" + /> +
+ + {/* Results count */} +
+ Showing {Math.min(filteredAndSortedConfigs.length, itemsPerPage)} of {filteredAndSortedConfigs.length} configurations +
+ + {/* Configurations Table */} +
+ + + + + + + + + {paginatedConfigs.map((config) => ( + + + + + ))} + +
+ Name + + Description +
+ + {config.gogen} + + +
{config.description || '-'}
+
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ +
+ )} + + {/* No results message */} + {filteredAndSortedConfigs.length === 0 && ( +
+ No configurations found matching your search. +
+ )} +
+ ); +}; + +export default ConfigurationList; \ No newline at end of file diff --git a/ui/src/components/ExecutionComponent.test.tsx b/ui/src/components/ExecutionComponent.test.tsx new file mode 100644 index 0000000..61064ae --- /dev/null +++ b/ui/src/components/ExecutionComponent.test.tsx @@ -0,0 +1,151 @@ +import { render, screen, fireEvent, act } from '../utils/test-utils'; +import ExecutionComponent from './ExecutionComponent'; +import gogenWasm from '../api/gogenWasm'; +import { Configuration } from '../api/gogenApi'; + +// Mock the gogenWasm module +jest.mock('../api/gogenWasm', () => ({ + __esModule: true, + default: { + executeConfiguration: jest.fn(), + }, +})); + +describe('ExecutionComponent', () => { + const mockConfig = { + gogen: 'test-config', + description: 'Test configuration', + config: 'config: test', + samples: [], + raters: [], + mix: [], + generators: [], + global: {}, + templates: [], + }; + + beforeEach(() => { + jest.clearAllMocks(); + // Mock the executeConfiguration function + (gogenWasm.executeConfiguration as jest.Mock).mockResolvedValue(['Test output']); + }); + + test('renders execution form', () => { + render(); + + expect(screen.getByLabelText(/events per interval/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/intervals/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/interval \(in seconds\)/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/output template/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/output mode/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /execute/i })).toBeInTheDocument(); + }); + + test('validates input fields', async () => { + render(); + + const eventCountInput = screen.getByLabelText(/events per interval/i); + const intervalsInput = screen.getByLabelText(/intervals/i); + const intervalSecondsInput = screen.getByLabelText(/interval \(in seconds\)/i); + const executeButton = screen.getByRole('button', { name: /execute/i }); + + // Test invalid inputs + await act(async () => { + fireEvent.change(eventCountInput, { target: { value: 'abc' } }); + fireEvent.change(intervalsInput, { target: { value: '-1' } }); + fireEvent.change(intervalSecondsInput, { target: { value: 'xyz' } }); + }); + + // The execute button should still be enabled as the component handles invalid inputs by ignoring them + expect(executeButton).not.toBeDisabled(); + + // Test valid inputs + await act(async () => { + fireEvent.change(eventCountInput, { target: { value: '100' } }); + fireEvent.change(intervalsInput, { target: { value: '2' } }); + fireEvent.change(intervalSecondsInput, { target: { value: '5' } }); + }); + + expect(executeButton).not.toBeDisabled(); + }); + + test('executes configuration in terminal mode', async () => { + render(); + + const executeButton = screen.getByRole('button', { name: /execute/i }); + + await act(async () => { + fireEvent.click(executeButton); + }); + + // Terminal container should be visible + expect(screen.getByTestId('terminal-container')).toBeInTheDocument(); + + // Should have called executeConfiguration with correct params + expect(gogenWasm.executeConfiguration).toHaveBeenCalledWith( + mockConfig, + { + eventCount: 1, + intervals: 5, + intervalSeconds: 1, + outputTemplate: 'raw' + }, + expect.any(Function) // Terminal mode uses callback + ); + }); + + test('executes configuration in structured mode', async () => { + render(); + + // Switch to structured mode + const outputModeSelect = screen.getByLabelText(/output mode/i); + await act(async () => { + fireEvent.change(outputModeSelect, { target: { value: 'structured' } }); + }); + + const executeButton = screen.getByRole('button', { name: /execute/i }); + await act(async () => { + fireEvent.click(executeButton); + }); + + // Should have called executeConfiguration without callback + expect(gogenWasm.executeConfiguration).toHaveBeenCalledWith( + mockConfig, + { + eventCount: 1, + intervals: 5, + intervalSeconds: 1, + outputTemplate: 'raw' + } + ); + }); + + it('handles execution errors', async () => { + const errorMessage = 'Test error'; + const mockExecute = gogenWasm.executeConfiguration as jest.Mock; + mockExecute.mockRejectedValueOnce(new Error(errorMessage)); + + const mockConfig: Configuration = { + gogen: 'test', + description: 'Test description', + config: 'test config', + samples: [], + raters: [], + mix: [], + generators: [], + templates: [] + }; + + render(); + + const executeButton = screen.getByText('Execute'); + await act(async () => { + fireEvent.click(executeButton); + }); + + // Error message should be displayed in the terminal + const terminalContainer = screen.getByTestId('terminal-container'); + expect(terminalContainer).toBeInTheDocument(); + expect(mockExecute).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/ui/src/components/ExecutionComponent.tsx b/ui/src/components/ExecutionComponent.tsx new file mode 100644 index 0000000..1487d6c --- /dev/null +++ b/ui/src/components/ExecutionComponent.tsx @@ -0,0 +1,301 @@ +import { useState, useEffect, useRef } from 'react'; +import { Terminal } from 'xterm'; +import gogenWasm, { ExecutionParams } from '../api/gogenWasm'; +import { Configuration } from '../api/gogenApi'; +import 'xterm/css/xterm.css'; + +// Helper function to check if a CSS file is loaded +const isCssLoaded = (href: string): boolean => { + const links = document.getElementsByTagName('link'); + for (let i = 0; i < links.length; i++) { + if (links[i].href.includes(href)) { + return true; + } + } + return false; +}; + +// Helper function to dynamically load a script +const loadScript = (src: string): Promise => { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.onload = () => resolve(); + script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); + document.head.appendChild(script); + }); +}; + +interface ExecutionComponentProps { + configuration: Configuration; +} + +const ExecutionComponent: React.FC = ({ configuration }) => { + const [isExecuting, setIsExecuting] = useState(false); + const [outputMode, setOutputMode] = useState<'terminal' | 'structured'>('terminal'); + const [structuredOutput, setStructuredOutput] = useState([]); + const [eventCount, setEventCount] = useState(1); + const [intervals, setIntervals] = useState(5); + const [intervalSeconds, setIntervalSeconds] = useState(1); + const [outputTemplate, setOutputTemplate] = useState<'raw' | 'json' | 'configured'>('raw'); + const [error, setError] = useState(null); + + const terminalRef = useRef(null); + const terminalInstance = useRef(null); + + // Initialize terminal + useEffect(() => { + // Clean up function to properly dispose of terminal + const cleanupTerminal = () => { + if (terminalInstance.current) { + terminalInstance.current.dispose(); + terminalInstance.current = null; + } + if (terminalRef.current) { + while (terminalRef.current.firstChild) { + terminalRef.current.removeChild(terminalRef.current.firstChild); + } + } + }; + + // Only initialize if we're in terminal mode and don't have an instance + if (outputMode === 'terminal') { + // Clean up any existing terminal first + cleanupTerminal(); + + const initializeTerminal = async () => { + try { + // Ensure the CSS is loaded + if (!isCssLoaded('xterm.css')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = 'https://unpkg.com/xterm@5.3.0/css/xterm.css'; + document.head.appendChild(link); + } + + // Check if Terminal class is available + if (typeof Terminal === 'undefined') { + try { + await loadScript('https://unpkg.com/xterm@5.3.0/lib/xterm.js'); + } catch (error) { + setError('Terminal component failed to load. Please refresh the page or try again later.'); + return; + } + } + + // Create new terminal instance + if (terminalRef.current && !terminalInstance.current) { + const term = new Terminal({ + cursorBlink: false, + disableStdin: true, + rows: 20, + cols: 100, + theme: { + background: '#f8f9fa', + foreground: '#212529', + }, + }); + + term.open(terminalRef.current); + + terminalInstance.current = term; + } + } catch (error) { + setError(`Terminal initialization error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + initializeTerminal(); + } else { + // Clean up terminal when switching to structured mode + cleanupTerminal(); + } + + // Cleanup on unmount or mode change + return cleanupTerminal; + }, [outputMode]); + + // Execute configuration + const executeConfiguration = async () => { + setIsExecuting(true); + + const executionParams: ExecutionParams = { + eventCount, + intervals, + intervalSeconds, + outputTemplate + }; + + try { + if (outputMode === 'terminal' && terminalInstance.current) { + terminalInstance.current.clear(); + + try { + await gogenWasm.executeConfiguration( + configuration, + executionParams, + (line) => { + if (terminalInstance.current) { + terminalInstance.current.writeln(line); + } + } + ); + + } catch (execError: any) { + terminalInstance.current.writeln(`\x1b[31mExecution error: ${execError.message || 'Unknown error'}\x1b[0m`); + } + } else if (outputMode === 'structured') { + try { + const results = await gogenWasm.executeConfiguration(configuration, executionParams); + const parsedResults = results.map((line) => { + if (typeof line === 'object') return line; + try { + return JSON.parse(line); + } catch (e) { + return { _raw: line }; + } + }); + setStructuredOutput(parsedResults); + } catch (execError: any) { + setError(`Error during structured execution: ${execError.message || 'Unknown error'}`); + } + } + } catch (error: any) { + setError(`Error: ${error.message || 'Unknown error'}`); + if (terminalInstance.current) { + terminalInstance.current.writeln(`\x1b[31mError: ${error.message || 'Unknown error'}\x1b[0m`); + terminalInstance.current.writeln('\x1b[31mCheck the browser console for more details.\x1b[0m'); + } + } finally { + setIsExecuting(false); + } + }; + + return ( +
+

Execute Configuration

+ +
+
+ + { + const value = e.target.value; + if (value === '' || /^\d+$/.test(value)) { + setIntervals(value === '' ? 1 : parseInt(value, 10)); + } + }} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white text-gray-900" + /> +
+ +
+ + { + const value = e.target.value; + if (value === '' || /^\d+$/.test(value)) { + setEventCount(value === '' ? 1 : parseInt(value, 10)); + } + }} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white text-gray-900" + /> +
+ +
+ + { + const value = e.target.value; + if (value === '' || /^\d+$/.test(value)) { + setIntervalSeconds(value === '' ? 1 : parseInt(value, 10)); + } + }} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white text-gray-900" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + {error && ( +
+ {error} +
+ )} + + {outputMode === 'terminal' ? ( +
+
+
+ ) : ( +
+
+            {JSON.stringify(structuredOutput, null, 2)}
+          
+
+ )} +
+ ); +}; + +export default ExecutionComponent; \ No newline at end of file diff --git a/ui/src/components/Footer.test.tsx b/ui/src/components/Footer.test.tsx new file mode 100644 index 0000000..b9dd372 --- /dev/null +++ b/ui/src/components/Footer.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '../utils/test-utils'; +import Footer from './Footer'; + +describe('Footer', () => { + beforeEach(() => { + // Mock the current year to make tests deterministic + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-01-01')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('renders copyright text with current year', () => { + render(