From 68ed892f711462459a3ee04df94baaec8e12b359 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 02:39:20 +0200 Subject: [PATCH 01/93] this adds the core runfile logic from steamserverui --- src/config/steamserverui-getters.go | 15 + src/config/steamserverui-setter.go | 20 + src/config/steamserverui-vars.go | 5 + src/config/vars.go | 1 + src/core/loader/runfile.go | 46 +++ src/logger/logger.go | 2 + .../runfilemanager/argexample.go | 51 +++ src/steamserverui/runfilemanager/args.go | 390 ++++++++++++++++++ src/steamserverui/runfilemanager/getters.go | 112 +++++ src/web/routes.go | 10 + src/web/runfile.go | 306 ++++++++++++++ 11 files changed, 958 insertions(+) create mode 100644 src/config/steamserverui-getters.go create mode 100644 src/config/steamserverui-setter.go create mode 100644 src/config/steamserverui-vars.go create mode 100644 src/core/loader/runfile.go create mode 100644 src/steamserverui/runfilemanager/argexample.go create mode 100644 src/steamserverui/runfilemanager/args.go create mode 100644 src/steamserverui/runfilemanager/getters.go create mode 100644 src/web/runfile.go diff --git a/src/config/steamserverui-getters.go b/src/config/steamserverui-getters.go new file mode 100644 index 00000000..7b4138c3 --- /dev/null +++ b/src/config/steamserverui-getters.go @@ -0,0 +1,15 @@ +package config + +// GetRunFilesFolder returns the RunFilesFolder +func GetRunFilesFolder() string { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return RunFilesFolder +} + +// GetRunfileGame returns the RunfileGame +func GetRunfileGame() string { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return RunfileGame +} diff --git a/src/config/steamserverui-setter.go b/src/config/steamserverui-setter.go new file mode 100644 index 00000000..d79fde4e --- /dev/null +++ b/src/config/steamserverui-setter.go @@ -0,0 +1,20 @@ +package config + +import ( + "fmt" + "strings" +) + +// SetRunfileGame sets the RunfileGame with validation +func SetRunfileGame(value string) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + if strings.TrimSpace(value) == "" { + return fmt.Errorf("runfile game cannot be empty") + } + + RunfileGame = value + return nil + //return saveConfig() +} diff --git a/src/config/steamserverui-vars.go b/src/config/steamserverui-vars.go new file mode 100644 index 00000000..e817e5a2 --- /dev/null +++ b/src/config/steamserverui-vars.go @@ -0,0 +1,5 @@ +package config + +var ( + RunfileGame string +) diff --git a/src/config/vars.go b/src/config/vars.go index a2f3a507..6db79528 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -143,6 +143,7 @@ var ( SSCMWebDir = "./UIMod/sscm/" SSCMFilePath = "./BepInEx/plugins/SSCM/SSCM.socket" SSCMPluginDir = "./BepInEx/plugins/SSCM/" + RunFilesFolder = "./UIMod/runfiles/" ) // Bundled Assets diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go new file mode 100644 index 00000000..32a21c9d --- /dev/null +++ b/src/core/loader/runfile.go @@ -0,0 +1,46 @@ +package loader + +import ( + "fmt" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfilemanager" +) + +// used via Runfile Gallery +func InitRunfile(game string) error { + // Validate input + game = strings.TrimSpace(game) + if game == "" { + return fmt.Errorf("game cannot be empty") + } + + logger.Runfile.Info("Updating runfile game to " + game) + logger.Runfile.Info("Stopping server if running") + gamemgr.InternalStopServer() + config.SetRunfileGame(game) + + if err := ReloadRunfile(); err != nil { + return err + } + + logger.Runfile.Info("Running SteamCMD, this may take a while...") + //steammgr.RunSteamCMD() + logger.Runfile.Warn("Steamcmd for runfile not implemented yet") + logger.Runfile.Info("Runfile game updated to " + game) + + return nil +} + +// used to only reload runfile into memory. Can be triggered from v1 UI -> Runfile Reset terminal +func ReloadRunfile() error { + if err := runfilemanager.LoadRunfile(config.GetRunfileGame(), config.GetRunFilesFolder()); err != nil { + logger.Runfile.Warn("Failed to reload runfile: " + err.Error()) + return err + } + logger.Runfile.Info("Runfile reloaded successfully") + return nil +} diff --git a/src/logger/logger.go b/src/logger/logger.go index 958d3971..96ed57eb 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -24,6 +24,7 @@ var ( SSE = &Logger{suffix: SYS_SSE} Security = &Logger{suffix: SYS_SECURITY} Localization = &Logger{suffix: SYS_LOCALIZATION} + Runfile = &Logger{suffix: SYS_RUNFILE} ) // Severity Levels @@ -48,6 +49,7 @@ const ( SYS_SSE = "SSE" SYS_SECURITY = "SECURITY" SYS_LOCALIZATION = "LOCALIZATION" + SYS_RUNFILE = "RUNFILE" ) const ( diff --git a/src/steamserverui/runfilemanager/argexample.go b/src/steamserverui/runfilemanager/argexample.go new file mode 100644 index 00000000..63279371 --- /dev/null +++ b/src/steamserverui/runfilemanager/argexample.go @@ -0,0 +1,51 @@ +package runfilemanager + +import ( + "fmt" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +// unused +func Examples() { + // Load gameTemplate into the global CurrentRunfile + err := LoadRunfile("Stationeers", config.GetRunFilesFolder()) + if err != nil { + panic(err) + } + + // Discover all available arguments + allArgs := GetAllArgs() + fmt.Println("Available arguments:") + for _, arg := range allArgs { + fmt.Printf("%s (%s): %s\n", arg.Flag, arg.Type, arg.Description) + } + + // Get arguments for UI display + groups := GetUIGroups() + for _, group := range groups { + fmt.Printf("\n%s Settings:\n", group) + for _, arg := range GetArgsByGroup(group) { + fmt.Printf("- %s: %s (Current: %s)\n", arg.UILabel, arg.Description, arg.RuntimeValue) + } + } + + // Update a parameter + if err := SetArgValue("GamePort", "28015"); err != nil { + panic(err) + } + + // Build final command line + args, err := BuildCommandArgs() + if err != nil { + panic(err) + } + fmt.Println("\nCommand line:", args) + + arg, err := GetSingleArg("someflag") + if err != nil { + fmt.Println(err) + return + } + fmt.Println(arg.RuntimeValue) // Access the argument's properties +} diff --git a/src/steamserverui/runfilemanager/args.go b/src/steamserverui/runfilemanager/args.go new file mode 100644 index 00000000..275f8861 --- /dev/null +++ b/src/steamserverui/runfilemanager/args.go @@ -0,0 +1,390 @@ +package runfilemanager + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// Package-level mutex for file operations +var runfileMutex sync.Mutex + +var CurrentRunfile *RunFile + +// Custom error types +type ErrRunfileNotLoaded struct{ Msg string } + +func (e ErrRunfileNotLoaded) Error() string { return e.Msg } + +type ErrArgNotFound struct{ Flag string } + +func (e ErrArgNotFound) Error() string { return fmt.Sprintf("argument %s not found", e.Flag) } + +type ErrInvalidGameName struct{ Name string } + +func (e ErrInvalidGameName) Error() string { + return fmt.Sprintf("invalid game name %q: must start with uppercase letter, no spaces, alphanumeric", e.Name) +} + +type ErrUnsetIdentifier struct{ Name string } + +func (e ErrUnsetIdentifier) Error() string { + return fmt.Sprintf("undefined runfile Identifier %q: If this is a first time setup, you can safely ignore this warning and proceed to select a runfile from the Runfile Gallery on your UI", e.Name) +} + +type ErrValidation struct { + Issues []string +} + +func (e ErrValidation) Error() string { + return fmt.Sprintf("validation failed: %s", strings.Join(e.Issues, "; ")) +} + +type GameArg struct { + Flag string `json:"flag"` + DefaultValue string `json:"default"` + RuntimeValue string `json:"-"` + Required bool `json:"required"` + RequiresValue bool `json:"requires_value"` + Description string `json:"description"` + Type string `json:"type"` + Special string `json:"special,omitempty"` + UILabel string `json:"ui_label"` + UIGroup string `json:"ui_group"` + Weight int `json:"weight"` + Min int `json:"min,omitempty"` + Max int `json:"max,omitempty"` + Disabled bool `json:"disabled,omitempty"` +} + +type Meta struct { + Name string `json:"name"` // SSUI Specific Game Identifier, must match the one in the filename. + Version string `json:"version"` // Runfile version +} + +type RunFile struct { + Meta Meta `json:"meta"` + Architecture string `json:"architecture,omitempty"` + SteamAppID string `json:"steam_app_id"` + SteamLoginRequired bool `json:"steam_login_required,omitempty"` //unused & unsupported, will later be used in combination with some way to provide a steam login + WindowsExecutable string `json:"windows_executable"` + LinuxExecutable string `json:"linux_executable"` + Args map[string][]GameArg `json:"args"` +} + +// Validate checks the RunFile state +func (rf *RunFile) Validate() error { + var issues []string + + // Validate SteamAppID: non-empty, numeric + if rf.SteamAppID == "" { + issues = append(issues, "SteamAppID is required") + } else if _, err := strconv.Atoi(rf.SteamAppID); err != nil { + issues = append(issues, fmt.Sprintf("SteamAppID must be numeric, got %s", rf.SteamAppID)) + } + + // Validate WindowsExecutable: if non-empty, must end with .exe + if rf.WindowsExecutable != "" && !strings.HasSuffix(strings.ToLower(rf.WindowsExecutable), ".exe") { + issues = append(issues, fmt.Sprintf("WindowsExecutable must end with .exe, got %s", rf.WindowsExecutable)) + } + + // Validate LinuxExecutable: if non-empty, must not end with .exe + if rf.LinuxExecutable != "" && strings.HasSuffix(strings.ToLower(rf.LinuxExecutable), ".exe") { + issues = append(issues, fmt.Sprintf("LinuxExecutable must not end with .exe, got %s", rf.LinuxExecutable)) + } + + // Validate Meta: ensure Name is non-empty + if rf.Meta.Name == "" { + issues = append(issues, "Meta.Name is required") + } + + // Validate args + for _, arg := range rf.getAllArgs() { + if arg.Disabled { + continue + } + if arg.Required && arg.RequiresValue && arg.RuntimeValue == "" { + issues = append(issues, fmt.Sprintf("required argument %s has no value", arg.Flag)) + } + switch arg.Type { + case "int": + if arg.RuntimeValue != "" { + if _, err := strconv.Atoi(arg.RuntimeValue); err != nil { + issues = append(issues, fmt.Sprintf("invalid integer value for %s: %s", arg.Flag, arg.RuntimeValue)) + } + } + case "bool": + if arg.RuntimeValue != "" && arg.RuntimeValue != "true" && arg.RuntimeValue != "false" { + issues = append(issues, fmt.Sprintf("invalid boolean value for %s: %s", arg.Flag, arg.RuntimeValue)) + } + } + } + + if len(issues) > 0 { + return ErrValidation{Issues: issues} + } + return nil +} + +// getAllArgs returns all GameArgs (internal method for validation) +func (rf *RunFile) getAllArgs() []GameArg { + var allArgs []GameArg + for _, category := range []string{"basic", "network", "advanced"} { + if args, exists := rf.Args[category]; exists { + allArgs = append(allArgs, args...) + } + } + return allArgs +} + +// LoadRunfile loads the runfile and stores it in CurrentRunfile +func LoadRunfile(gameName, runFilesFolder string) error { + runfileMutex.Lock() + defer runfileMutex.Unlock() + + // check if gameName is set to empty string + if gameName == "" { + err := ErrUnsetIdentifier{Name: gameName} + return err + } + + // Edge case: empty runFilesFolder Setting + if runFilesFolder == "" { + err := fmt.Errorf("runFilesFolder cannot be empty") + logger.Runfile.Error(err.Error()) + return err + } + + // Edge case: validate gameName (uppercase first letter, no spaces, alphanumeric) + if gameName == "" || !regexp.MustCompile(`^[A-Z][a-zA-Z0-9]*$`).MatchString(gameName) { + err := ErrInvalidGameName{Name: gameName} + logger.Runfile.Error(err.Error()) + return err + } + + filePath := filepath.Join(runFilesFolder, fmt.Sprintf("run%s.ssui", gameName)) + logger.Runfile.Debug(fmt.Sprintf("loading runfile: path=%s", filePath)) + + fileData, err := os.ReadFile(filePath) + if err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to read runfile: path=%s, error=%v", filePath, err)) + return fmt.Errorf("failed to read runfile: %w", err) + } + + var runfile RunFile + if err := json.Unmarshal(fileData, &runfile); err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to parse runfile: path=%s, error=%v", filePath, err)) + return fmt.Errorf("failed to parse runfile: %w", err) + } + + // Check executable availability + if _, err := runfile.GetExecutable(); err != nil { + logger.Runfile.Debug(fmt.Sprintf("executable validation failed: error=%v", err)) + return err + } + + // Initialize runtime values *before* validation + for category := range runfile.Args { + for i := range runfile.Args[category] { + runfile.Args[category][i].RuntimeValue = runfile.Args[category][i].DefaultValue + logger.Runfile.Debug(fmt.Sprintf("initialized arg: flag=%s, default=%s, runtime=%s", + runfile.Args[category][i].Flag, + runfile.Args[category][i].DefaultValue, + runfile.Args[category][i].RuntimeValue)) + } + } + + // Validate runfile + if err := runfile.Validate(); err != nil { + logger.Runfile.Error(fmt.Sprintf("runfile validation failed: path=%s, error=%v", filePath, err)) + CurrentRunfile = nil // Ensure no partial state + return err + } + + CurrentRunfile = &runfile + logger.Runfile.Info(fmt.Sprintf("runfile loaded: path=%s", filePath)) + return nil +} + +// SaveRunfile persists the current RunFile to disk +func SaveRunfile() error { + runfileMutex.Lock() + defer runfileMutex.Unlock() + + if CurrentRunfile == nil { + err := ErrRunfileNotLoaded{Msg: "runfile not loaded"} + logger.Runfile.Error(err.Error()) + return err + } + + // Build filepath + filePath := filepath.Join(config.GetRunFilesFolder(), fmt.Sprintf("run%s.ssui", config.GetRunfileGame())) + logger.Runfile.Debug(fmt.Sprintf("saving runfile: path=%s", filePath)) + + // Update DefaultValue from RuntimeValue + for category := range CurrentRunfile.Args { + for i := range CurrentRunfile.Args[category] { + CurrentRunfile.Args[category][i].DefaultValue = CurrentRunfile.Args[category][i].RuntimeValue + } + } + + // Validate state + if err := CurrentRunfile.Validate(); err != nil { + logger.Runfile.Error(fmt.Sprintf("runfile validation failed: path=%s, error=%v", filePath, err)) + return err + } + + // Serialize to JSON + data, err := json.MarshalIndent(CurrentRunfile, "", " ") + if err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to serialize runfile: path=%s, error=%v", filePath, err)) + return fmt.Errorf("failed to serialize runfile: %w", err) + } + + // Write to file with retries + const maxRetries = 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + if err := os.WriteFile(filePath, data, 0644); err != nil { + logger.Runfile.Warn(fmt.Sprintf("failed to write runfile: path=%s, attempt=%d, error=%v", filePath, attempt, err)) + if attempt == maxRetries { + logger.Runfile.Error(fmt.Sprintf("failed to write runfile after %d attempts: path=%s, error=%v", maxRetries, filePath, err)) + return fmt.Errorf("failed to write runfile after %d attempts: %w", maxRetries, err) + } + time.Sleep(100 * time.Millisecond) + continue + } + break + } + + logger.Runfile.Info(fmt.Sprintf("runfile saved: path=%s", filePath)) + return nil +} + +// SetArgValue updates an argument's runtime value and saves the runfile +func SetArgValue(flag string, value string) error { + if CurrentRunfile == nil { + err := ErrRunfileNotLoaded{Msg: "runfile not loaded"} + logger.Runfile.Error(err.Error()) + return err + } + + for category := range CurrentRunfile.Args { + for i := range CurrentRunfile.Args[category] { + if CurrentRunfile.Args[category][i].Flag != flag { + continue + } + + // Validate value + arg := CurrentRunfile.Args[category][i] + switch arg.Type { + case "int": + if _, err := strconv.Atoi(value); err != nil { + err := ErrValidation{Issues: []string{fmt.Sprintf("invalid integer value for %s: %s", flag, value)}} + logger.Runfile.Error(fmt.Sprintf("validation failed: flag=%s, value=%s, error=%v", flag, value, err)) + return err + } + case "bool": + if value != "true" && value != "false" { + err := ErrValidation{Issues: []string{fmt.Sprintf("invalid boolean value for %s: %s", flag, value)}} + logger.Runfile.Error(fmt.Sprintf("validation failed: flag=%s, value=%s, error=%v", flag, value, err)) + return err + } + } + + // Transactional update + originalValue := arg.RuntimeValue // Clone state + CurrentRunfile.Args[category][i].RuntimeValue = value + if err := SaveRunfile(); err != nil { + // Rollback on failure + CurrentRunfile.Args[category][i].RuntimeValue = originalValue + logger.Runfile.Error(fmt.Sprintf("failed to save runfile: flag=%s, value=%s, error=%v", flag, value, err)) + return fmt.Errorf("failed to save runfile: %w", err) + } + + logger.Runfile.Debug(fmt.Sprintf("set arg: flag=%s, value=%s", flag, value)) + return nil + } + } + + err := ErrArgNotFound{Flag: flag} + logger.Runfile.Error(fmt.Sprintf("arg not found: flag=%s", flag)) + return err +} + +// BuildCommandArgs builds the command-line arguments +func BuildCommandArgs() ([]string, error) { + if CurrentRunfile == nil { + err := ErrRunfileNotLoaded{Msg: "no runfile is currently loaded"} + logger.Runfile.Error(err.Error()) + return nil, err + } + + // Validate before building + if err := CurrentRunfile.Validate(); err != nil { + logger.Runfile.Error(fmt.Sprintf("runfile validation failed: error=%v", err)) + return nil, err + } + + var args []string + allArgs := CurrentRunfile.getAllArgs() + + // Sort by weight (primary) and UIGroup (secondary) + sort.Slice(allArgs, func(i, j int) bool { + if allArgs[i].Weight != allArgs[j].Weight { + return allArgs[i].Weight < allArgs[j].Weight + } + return switchCategoryWeight(allArgs[i].UIGroup) < switchCategoryWeight(allArgs[j].UIGroup) + }) + + for _, arg := range allArgs { + if arg.Disabled { + continue + } + if !arg.Required && arg.RequiresValue && arg.RuntimeValue == "" { + continue + } + args = append(args, arg.Flag) + + // Special handling + if arg.Special == "space_delimited" { + parts := strings.Split(arg.RuntimeValue, " ") + for _, part := range parts { + if part != "" { + args = append(args, part) + } + } + continue + } + + // Only add value if the argument requires one + if arg.RequiresValue && arg.RuntimeValue != "" { + args = append(args, arg.RuntimeValue) + } + } + + return args, nil +} + +// switchCategoryWeight maps UIGroup to a weight for sorting +func switchCategoryWeight(group string) int { + switch group { + case "Basic": + return 0 + case "Network": + return 1 + case "Advanced": + return 2 + default: + return 3 + } +} diff --git a/src/steamserverui/runfilemanager/getters.go b/src/steamserverui/runfilemanager/getters.go new file mode 100644 index 00000000..6fabab6d --- /dev/null +++ b/src/steamserverui/runfilemanager/getters.go @@ -0,0 +1,112 @@ +package runfilemanager + +import ( + "fmt" + "runtime" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// GetAllArgs returns all GameArgs from the runfile +func GetAllArgs() []GameArg { + if CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + return nil + } + return CurrentRunfile.getAllArgs() +} + +func GetUIGroups() []string { + if CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + return nil + } + + groups := make(map[string]bool) + for _, arg := range CurrentRunfile.getAllArgs() { + groups[arg.UIGroup] = true + } + + var result []string + for group := range groups { + result = append(result, group) + } + return result +} + +func GetArgsByGroup(group string) []GameArg { + if CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + return nil + } + + var result []GameArg + for _, arg := range CurrentRunfile.getAllArgs() { + if arg.UIGroup == group { + result = append(result, arg) + } + } + return result +} + +// GetSingleArg retrieves a specific GameArg by its flag +func GetSingleArg(flag string) (*GameArg, error) { + if CurrentRunfile == nil { + err := ErrRunfileNotLoaded{Msg: "runfile not loaded"} + logger.Runfile.Error(err.Error()) + return nil, err + } + + for _, arg := range CurrentRunfile.getAllArgs() { + if arg.Flag == flag { + return &arg, nil + } + } + + err := ErrArgNotFound{Flag: flag} + logger.Runfile.Error(fmt.Sprintf("arg not found: flag=%s", flag)) + return nil, err +} + +// GetExecutable returns the appropriate executable based on GOOS or Architecture +func (rf *RunFile) GetExecutable() (string, error) { + goos := strings.ToLower(runtime.GOOS) + + // If Architecture is not all, use it exclusively + if rf.Architecture != "all" && rf.Architecture != "" { + arch := strings.ToLower(rf.Architecture) + if arch != "windows" && arch != "linux" { + return "", fmt.Errorf("invalid architecture in runfile: %s", rf.Architecture) + } + if arch != goos { + return "", fmt.Errorf("runfile architecture %s does not match current OS %s", arch, goos) + } + if arch == "windows" { + if rf.WindowsExecutable == "" { + return "", fmt.Errorf("WindowsExecutable is required when architecture is set to windows") + } + return rf.WindowsExecutable, nil + } + if rf.LinuxExecutable == "" { + return "", fmt.Errorf("LinuxExecutable is required when architecture is set to linux") + } + return rf.LinuxExecutable, nil + } + + // If Architecture is all or empty, select based on GOOS + + if goos == "windows" { + if rf.WindowsExecutable == "" { + return "", fmt.Errorf("WindowsExecutable is required for windows OS") + } + return rf.WindowsExecutable, nil + } + if goos == "linux" { + if rf.LinuxExecutable == "" { + return "", fmt.Errorf("LinuxExecutable is required for linux OS") + } + return rf.LinuxExecutable, nil + } + return "", fmt.Errorf("unsupported OS: %s", goos) +} diff --git a/src/web/routes.go b/src/web/routes.go index 9b7a45eb..9c4f02a3 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -79,5 +79,15 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/auth/setup/register", RegisterUserHandler) // user registration protectedMux.HandleFunc("/api/v2/auth/setup/finalize", SetupFinalizeHandler) + // SteamServerUI + + protectedMux.HandleFunc("/api/v2/runfile/groups", HandleRunfileGroups) + protectedMux.HandleFunc("/api/v2/runfile/args", HandleRunfileArgs) + protectedMux.HandleFunc("/api/v2/runfile/args/update", HandleRunfileArgUpdate) + protectedMux.HandleFunc("/api/v2/runfile", HandleRunfile) + protectedMux.HandleFunc("/api/v2/runfile/save", HandleRunfileSave) + protectedMux.HandleFunc("/api/v2/runfile/hardreset", HandleSetRunfileGame) + protectedMux.HandleFunc("/api/v2/loader/reloadrunfile", HandleReloadRunfile) + return mux, protectedMux } diff --git a/src/web/runfile.go b/src/web/runfile.go new file mode 100644 index 00000000..3c7eea67 --- /dev/null +++ b/src/web/runfile.go @@ -0,0 +1,306 @@ +package web + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfilemanager" +) + +// APIGameArg is a DTO for GameArg, including RuntimeValue and all fields +type APIGameArg struct { + Flag string `json:"flag"` + DefaultValue string `json:"default"` + RuntimeValue string `json:"runtime_value"` + Required bool `json:"required"` + RequiresValue bool `json:"requires_value"` + Description string `json:"description"` + Type string `json:"type"` + Special string `json:"special,omitempty"` + UILabel string `json:"ui_label"` + UIGroup string `json:"ui_group"` + Weight int `json:"weight"` + Min int `json:"min,omitempty"` + Max int `json:"max,omitempty"` + Disabled bool `json:"disabled"` +} + +// APIMeta mirrors runfilemanager.Meta for API responses +type APIMeta struct { + Name string `json:"name"` + Version string `json:"version"` +} + +// APIRunFile is a DTO for RunFile, using APIGameArg and APIMeta +type APIRunFile struct { + Meta APIMeta `json:"meta"` + Architecture string `json:"architecture,omitempty"` + SteamAppID string `json:"steam_app_id"` + WindowsExecutable string `json:"windows_executable"` + LinuxExecutable string `json:"linux_executable"` + Args map[string][]APIGameArg `json:"args"` +} + +// apiResponse is the standard JSON response format +type apiResponse struct { + Data interface{} `json:"data"` + Error string `json:"error,omitempty"` +} + +// toAPIGameArg converts runfilemanager.GameArg to APIGameArg +func toAPIGameArg(arg runfilemanager.GameArg) APIGameArg { + return APIGameArg{ + Flag: arg.Flag, + DefaultValue: arg.DefaultValue, + RuntimeValue: arg.RuntimeValue, + Required: arg.Required, + RequiresValue: arg.RequiresValue, + Description: arg.Description, + Type: arg.Type, + Special: arg.Special, + UILabel: arg.UILabel, + UIGroup: arg.UIGroup, + Weight: arg.Weight, + Min: arg.Min, + Max: arg.Max, + Disabled: arg.Disabled, + } +} + +// toAPIRunFile converts runfilemanager.RunFile to APIRunFile +func toAPIRunFile(rf *runfilemanager.RunFile) APIRunFile { + apiArgs := make(map[string][]APIGameArg) + for category, args := range rf.Args { + for _, arg := range args { + apiArgs[category] = append(apiArgs[category], toAPIGameArg(arg)) + } + } + return APIRunFile{ + Meta: APIMeta{ + Name: rf.Meta.Name, + Version: rf.Meta.Version, + }, + Architecture: rf.Architecture, + SteamAppID: rf.SteamAppID, + WindowsExecutable: rf.WindowsExecutable, + LinuxExecutable: rf.LinuxExecutable, + Args: apiArgs, + } +} + +// writeJSONResponse writes a JSON response with the given status code +func writeJSONResponse(w http.ResponseWriter, status int, data interface{}, errMsg string) { + resp := apiResponse{Data: data, Error: errMsg} + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(resp); err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to write JSON response: %v", err)) + } +} + +// HandleRunfileGroups handles GET /api/v2/runfile/groups +func HandleRunfileGroups(w http.ResponseWriter, r *http.Request) { + logger.Runfile.Debug("GET /api/v2/runfile/groups") + if runfilemanager.CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") + return + } + + groups := runfilemanager.GetUIGroups() + logger.Runfile.Info("fetched UI groups") + writeJSONResponse(w, http.StatusOK, groups, "") +} + +// HandleRunfileArgs handles GET /api/v2/runfile/args +func HandleRunfileArgs(w http.ResponseWriter, r *http.Request) { + group := r.URL.Query().Get("group") + logger.Runfile.Debug(fmt.Sprintf("GET /api/v2/runfile/args group=%s", group)) + + if runfilemanager.CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") + return + } + + var args []runfilemanager.GameArg + if group != "" { + // Validate group + validGroups := runfilemanager.GetUIGroups() + valid := false + for _, g := range validGroups { + if g == group { + valid = true + break + } + } + if !valid { + logger.Runfile.Error(fmt.Sprintf("invalid group: %s", group)) + writeJSONResponse(w, http.StatusBadRequest, nil, fmt.Sprintf("invalid group: %s", group)) + return + } + args = runfilemanager.GetArgsByGroup(group) + } else { + args = runfilemanager.GetAllArgs() + } + + // Convert to APIGameArg + apiArgs := make([]APIGameArg, len(args)) + for i, arg := range args { + apiArgs[i] = toAPIGameArg(arg) + } + + logger.Runfile.Info(fmt.Sprintf("fetched args for group=%s", group)) + writeJSONResponse(w, http.StatusOK, apiArgs, "") +} + +// HandleRunfileArgUpdate handles POST /api/v2/runfile/args/update +func HandleRunfileArgUpdate(w http.ResponseWriter, r *http.Request) { + logger.Runfile.Debug("POST /api/v2/runfile/args") + + if runfilemanager.CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") + return + } + + var req struct { + Flag string `json:"flag"` + Value string `json:"value"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Runfile.Error(fmt.Sprintf("invalid request body: %v", err)) + writeJSONResponse(w, http.StatusBadRequest, nil, "invalid request body") + return + } + + if req.Flag == "" { + logger.Runfile.Error("flag is required") + writeJSONResponse(w, http.StatusBadRequest, nil, "flag is required") + return + } + + if err := runfilemanager.SetArgValue(req.Flag, req.Value); err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to set arg %s: %v", req.Flag, err)) + writeJSONResponse(w, http.StatusBadRequest, nil, fmt.Sprintf("failed to set arg: %v", err)) + return + } + + logger.Runfile.Info(fmt.Sprintf("updated arg %s to %s", req.Flag, req.Value)) + writeJSONResponse(w, http.StatusOK, map[string]string{"flag": req.Flag, "value": req.Value}, "") +} + +// HandleRunfile handles GET /api/v2/runfile +func HandleRunfile(w http.ResponseWriter, r *http.Request) { + logger.Runfile.Debug("GET /api/v2/runfile") + + if runfilemanager.CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") + return + } + + apiRunfile := toAPIRunFile(runfilemanager.CurrentRunfile) + logger.Runfile.Info("fetched runfile") + writeJSONResponse(w, http.StatusOK, apiRunfile, "") +} + +// HandleRunfileSave handles POST /api/v2/runfile/save +func HandleRunfileSave(w http.ResponseWriter, r *http.Request) { + logger.Runfile.Debug("POST /api/v2/runfile/save") + + if runfilemanager.CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") + return + } + + if err := runfilemanager.SaveRunfile(); err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to save runfile: %v", err)) + writeJSONResponse(w, http.StatusInternalServerError, nil, fmt.Sprintf("failed to save runfile: %v", err)) + return + } + + logger.Runfile.Info("runfile saved") + writeJSONResponse(w, http.StatusOK, "runfile saved", "") +} + +// HandleSetRunfileGame reloads the runfile and restarts most of the server. It can also be used to reload the runfile from Disk as a hard reset. +func HandleSetRunfileGame(w http.ResponseWriter, r *http.Request) { + + reloadMu.Lock() + defer reloadMu.Unlock() + + // Restrict to POST method + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Read and validate request body + var request struct { + Game string `json:"game"` + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Validate input + game := strings.TrimSpace(request.Game) + if game == "" { + http.Error(w, "Game cannot be empty", http.StatusBadRequest) + return + } + + // Call InitRunfile to handle the runfile update + if err := loader.InitRunfile(game); err != nil { + logger.Core.Debug("Failed to initialize runfile: " + err.Error()) + http.Error(w, "Failed to initialize runfile: "+err.Error(), http.StatusInternalServerError) + return + } + + logger.Core.Info("Runfile game updated successfully to " + game) + + // Prepare response + response := struct { + Message string `json:"message"` + Game string `json:"game"` + }{ + Message: "Monitor console for update status", + Game: game, + } + + // Set response headers and write JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } +} + +func HandleReloadRunfile(w http.ResponseWriter, r *http.Request) { + logger.Web.Debug("Received reloadrunfile request from API") + reloadMu.Lock() + defer reloadMu.Unlock() + // accept only GET requests + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) + return + } + loader.ReloadRunfile() + + // Set response headers and write JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + if err := json.NewEncoder(w).Encode(map[string]string{"status": "OK"}); err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } +} From e47d89864f8d2e627a35611e624a75efe9185dce Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 02:55:47 +0200 Subject: [PATCH 02/93] this adds the settings implementaiton from SteamServerUI --- src/steamserverui/settings/retrieve.go | 345 +++++++++++++++++++++++++ src/steamserverui/settings/save.go | 326 +++++++++++++++++++++++ src/web/routes.go | 3 + 3 files changed, 674 insertions(+) create mode 100644 src/steamserverui/settings/retrieve.go create mode 100644 src/steamserverui/settings/save.go diff --git a/src/steamserverui/settings/retrieve.go b/src/steamserverui/settings/retrieve.go new file mode 100644 index 00000000..8cf82c71 --- /dev/null +++ b/src/steamserverui/settings/retrieve.go @@ -0,0 +1,345 @@ +package settings + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +// package settings handles API communication with the config values in package config via getter /setter functions. + +// ConfigSetting represents metadata for a configuration setting +type ConfigSetting struct { + Name string `json:"name"` + Type string `json:"type"` + Group string `json:"group"` + Description string `json:"description"` + Value interface{} `json:"value"` + Min *int `json:"min,omitempty"` + Max *int `json:"max,omitempty"` + Required bool `json:"required"` +} + +// ConfigSettingsResponse represents the API response +type ConfigSettingsResponse struct { + Data []ConfigSetting `json:"data"` + Error string `json:"error,omitempty"` +} + +func RetrieveSettings(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + + settings := []ConfigSetting{ + { + Name: "IsDebugMode", + Type: "bool", + Group: "System Settings", + Description: "Enable pprof server", + Value: config.GetIsDebugMode(), + }, + { + Name: "CreateSSUILogFile", + Type: "bool", + Group: "System Settings", + Description: "Create SSUI log files", + Value: config.GetCreateSSUILogFile(), + }, + { + Name: "LogLevel", + Type: "int", + Group: "System Settings", + Description: "Logging verbosity level", + Value: config.GetLogLevel(), + Min: intPtr(0), + }, + //{ + // Name: "BackendEndpointIP", + // Type: "string", + // Group: "System Settings", + // Description: "IP address for backend endpoint", + // Value: config.GetBackendEndpointIP(), + // Required: true, + //}, + { + Name: "BackendEndpointPort", + Type: "string", + Group: "System Settings", + Description: "Port for backend endpoint", + Value: config.GetSSUIWebPort(), // CHANGE VAR NAME TO MATCH (Get)BackendEndpointPort + Required: true, + }, + //{ + // Name: "BackupKeepLastN", + // Type: "int", + // Group: "Advanced Settings", + // Description: "Number of recent backups to keep", + // Value: config.GetBackupKeepLastN(), + // Min: intPtr(0), + //}, + //{ + // Name: "IsCleanupEnabled", + // Type: "bool", + // Group: "Advanced Settings", + // Description: "Enable backup cleanup", + // Value: config.GetIsCleanupEnabled(), + //}, + //{ + // Name: "BackupKeepDailyFor", + // Type: "int", + // Group: "Advanced Settings", + // Description: "Hours to keep daily backups", + // Value: int(config.GetBackupKeepDailyFor() / time.Hour), + // Min: intPtr(0), + //}, + //{ + // Name: "BackupKeepWeeklyFor", + // Type: "int", + // Group: "Advanced Settings", + // Description: "Hours to keep weekly backups", + // Value: int(config.GetBackupKeepWeeklyFor() / time.Hour), + // Min: intPtr(0), + //}, + //{ + // Name: "BackupKeepMonthlyFor", + // Type: "int", + // Group: "Advanced Settings", + // Description: "Hours to keep monthly backups", + // Value: int(config.GetBackupKeepMonthlyFor() / time.Hour), + // Min: intPtr(0), + //}, + //{ + // Name: "BackupCleanupInterval", + // Type: "int", + // Group: "Advanced Settings", + // Description: "Hours between backup cleanup runs", + // Value: int(config.GetBackupCleanupInterval() / time.Hour), + // Min: intPtr(0), + //}, + //{ + // Name: "BackupWaitTime", + // Type: "int", + // Group: "Advanced Settings", + // Description: "Seconds to wait before backup", + // Value: int(config.GetBackupWaitTime() / time.Second), + // Min: intPtr(0), + //}, + { + Name: "GameBranch", + Type: "string", + Group: "Update Settings", + Description: "Game branch for updates (Restart Required)", + Value: config.GetGameBranch(), + }, + //{ + // Name: "Users", + // Type: "map", + // Group: "Advanced Settings", + // Description: "User authentication mappings", + // Value: config.GetUsers(), + //}, + { + Name: "AuthEnabled", + Type: "bool", + Group: "System Settings", + Description: "Enable authentication", + Value: config.GetAuthEnabled(), + }, + { + Name: "JwtKey", + Type: "string", + Group: "Security Settings", + Description: "Encryption key for Authentication", + Value: config.GetJwtKey(), + }, + { + Name: "AuthTokenLifetime", + Type: "int", + Group: "Security Settings", + Description: "Token lifetime in seconds", + Value: config.GetAuthTokenLifetime(), + Min: intPtr(0), + }, + { + Name: "IsUpdateEnabled", + Type: "bool", + Group: "System Settings", + Description: "Enable automatic updates", + Value: config.GetIsUpdateEnabled(), + }, + //{ + // Name: "IsSSCMEnabled", + // Type: "bool", + // Group: "Advanced Settings", + // Description: "Enable SSCM integration", + // Value: config.GetIsSSCMEnabled(), + //}, + //{ + // Name: "IsCodeServerEnabled", + // Type: "bool", + // Group: "System Settings", + // Description: "Enables the Code Server integration. Linux only. Attention: Does NOT have an updater at the moment. Be careful with this feature.", + // Value: config.GetIsCodeServerEnabled(), + //}, + { + Name: "AllowPrereleaseUpdates", + Type: "bool", + Group: "Update Settings", + Description: "Allow prerelease updates", + Value: config.GetAllowPrereleaseUpdates(), + }, + { + Name: "AllowMajorUpdates", + Type: "bool", + Group: "Update Settings", + Description: "Allow major version updates", + Value: config.GetAllowMajorUpdates(), + }, + // Discord Settings + { + Name: "IsDiscordEnabled", + Type: "bool", + Group: "Discord Settings", + Description: "Enable Discord integration", + Value: config.GetIsDiscordEnabled(), + }, + { + Name: "DiscordToken", + Type: "string", + Group: "Discord Settings", + Description: "Discord bot token", + Value: config.GetDiscordToken(), + }, + { + Name: "ControlChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Control channel ID", + Value: config.GetControlChannelID(), + }, + { + Name: "StatusChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Status channel ID", + Value: config.GetStatusChannelID(), + }, + { + Name: "ConnectionListChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Connection list channel ID", + Value: config.GetConnectionListChannelID(), + }, + { + Name: "LogChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Log channel ID", + Value: config.GetLogChannelID(), + }, + { + Name: "SaveChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Save channel ID", + Value: config.GetSaveChannelID(), + }, + { + Name: "ControlPanelChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Control panel channel ID", + Value: config.GetControlPanelChannelID(), + }, + { + Name: "DiscordCharBufferSize", + Type: "int", + Group: "Discord Settings", + Description: "Discord character buffer size", + Value: config.GetDiscordCharBufferSize(), + }, + { + Name: "ErrorChannelID", + Type: "string", + Group: "Discord Settings", + Description: "Error channel ID", + Value: config.GetErrorChannelID(), + }, + //{ + // Name: "BackupContentDir", + // Type: "string", + // Group: "Backup Settings", + // Description: "Backup content directory", + // Value: config.GetBackupContentDir(), + //}, + //{ + // Name: "BackupsStoreDir", + // Type: "string", + // Group: "Backup Settings", + // Description: "Backup stored backups directory", + // Value: config.GetBackupsStoreDir(), + //}, + //{ + // Name: "BackupLoopInterval", + // Type: "string", + // Group: "Backup Settings", + // Description: "Backup loop interval", + // Value: config.GetBackupLoopInterval().String(), + //}, + //{ + // Name: "BackupMode", + // Type: "string", + // Group: "Backup Settings", + // Description: "Backup mode", + // Value: config.GetBackupMode(), + //}, + //{ + // Name: "BackupMaxFileSize", + // Type: "int", + // Group: "Backup Settings", + // Description: "Max file size", + // Value: config.GetBackupMaxFileSize(), + //}, + //{ + // Name: "BackupUseCompression", + // Type: "bool", + // Group: "Backup Settings", + // Description: "Use compression", + // Value: config.GetBackupUseCompression(), + //}, + //{ + // Name: "BackupKeepSnapshot", + // Type: "bool", + // Group: "Backup Settings", + // Description: "Keep snapshot", + // Value: config.GetBackupKeepSnapshot(), + //}, + { + Name: "IsConsoleEnabled", + Type: "bool", + Group: "System Settings", + Description: "Expose various actions directly in the command line (Restart Required)", + Value: config.GetIsConsoleEnabled(), + }, + } + + response := ConfigSettingsResponse{ + Data: settings, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) + return + } +} + +// intPtr creates a pointer to an int +func intPtr(i int) *int { + return &i +} diff --git a/src/steamserverui/settings/save.go b/src/steamserverui/settings/save.go new file mode 100644 index 00000000..ca857c27 --- /dev/null +++ b/src/steamserverui/settings/save.go @@ -0,0 +1,326 @@ +package settings + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" +) + +// package settings handles API communication with the config values in package config via getter /setter functions. + +// setterFunc defines the signature for setter functions +type setterFunc func(interface{}) error + +// setterMap maps JSON keys (global variable names) to setter functions with type checking +var setterMap = map[string]setterFunc{ + //"BackendEndpointIP": func(v interface{}) error { + // if str, ok := v.(string); ok { + // return config.SetBackendEndpointIP(str) + // } + // return fmt.Errorf("invalid type for BackendEndpointIP: expected string") + //}, + //"BackendEndpointPort": func(v interface{}) error { + // if str, ok := v.(string); ok { + // return config.SetBackendEndpointPort(str) + // } + // return fmt.Errorf("invalid type for BackendEndpointPort: expected string") + //}, + "DiscordToken": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetDiscordToken(str) + } + return fmt.Errorf("invalid type for DiscordToken: expected string") + }, + "ControlChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetControlChannelID(str) + } + return fmt.Errorf("invalid type for ControlChannelID: expected string") + }, + "StatusChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetStatusChannelID(str) + } + return fmt.Errorf("invalid type for StatusChannelID: expected string") + }, + "ConnectionListChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetConnectionListChannelID(str) + } + return fmt.Errorf("invalid type for ConnectionListChannelID: expected string") + }, + "LogChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetLogChannelID(str) + } + return fmt.Errorf("invalid type for LogChannelID: expected string") + }, + "SaveChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetSaveChannelID(str) + } + return fmt.Errorf("invalid type for SaveChannelID: expected string") + }, + "ControlPanelChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetControlPanelChannelID(str) + } + return fmt.Errorf("invalid type for ControlPanelChannelID: expected string") + }, + "DiscordCharBufferSize": func(v interface{}) error { + if f, ok := v.(float64); ok { + return config.SetDiscordCharBufferSize(int(f)) + } + return fmt.Errorf("invalid type for DiscordCharBufferSize: expected number") + }, + "BlackListFilePath": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetBlackListFilePath(str) + } + return fmt.Errorf("invalid type for BlackListFilePath: expected string") + }, + "IsDiscordEnabled": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetIsDiscordEnabled(b) + } + return fmt.Errorf("invalid type for IsDiscordEnabled: expected bool") + }, + "ErrorChannelID": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetErrorChannelID(str) + } + return fmt.Errorf("invalid type for ErrorChannelID: expected string") + }, + "GameBranch": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetGameBranch(str) + } + return fmt.Errorf("invalid type for GameBranch: expected string") + }, + "Users": func(v interface{}) error { + if m, ok := v.(map[string]interface{}); ok { + users := make(map[string]string) + for k, val := range m { + if strVal, ok := val.(string); ok { + users[k] = strVal + } else { + return fmt.Errorf("invalid value type for Users: expected string") + } + } + return config.SetUsers(users) + } + return fmt.Errorf("invalid type for Users: expected map[string]string") + }, + "AuthEnabled": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetAuthEnabled(b) + } + return fmt.Errorf("invalid type for AuthEnabled: expected bool") + }, + "JwtKey": func(v interface{}) error { + if str, ok := v.(string); ok { + return config.SetJwtKey(str) + } + return fmt.Errorf("invalid type for JwtKey: expected string") + }, + "AuthTokenLifetime": func(v interface{}) error { + if f, ok := v.(float64); ok { + return config.SetAuthTokenLifetime(int(f)) + } + return fmt.Errorf("invalid type for AuthTokenLifetime: expected number") + }, + "IsDebugMode": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetIsDebugMode(b) + } + return fmt.Errorf("invalid type for IsDebugMode: expected bool") + }, + "CreateSSUILogFile": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetCreateSSUILogFile(b) + } + return fmt.Errorf("invalid type for CreateSSUILogFile: expected bool") + }, + "LogLevel": func(v interface{}) error { + if f, ok := v.(float64); ok { + return config.SetLogLevel(int(f)) + } + return fmt.Errorf("invalid type for LogLevel: expected number") + }, + "SubsystemFilters": func(v interface{}) error { + if arr, ok := v.([]interface{}); ok { + filters := make([]string, 0, len(arr)) + for _, val := range arr { + if strVal, ok := val.(string); ok { + filters = append(filters, strVal) + } else { + return fmt.Errorf("invalid value type for SubsystemFilters: expected string") + } + } + return config.SetSubsystemFilters(filters) + } + return fmt.Errorf("invalid type for SubsystemFilters: expected array of strings") + }, + "IsUpdateEnabled": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetIsUpdateEnabled(b) + } + return fmt.Errorf("invalid type for IsUpdateEnabled: expected bool") + }, + "IsSSCMEnabled": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetIsSSCMEnabled(b) + } + return fmt.Errorf("invalid type for IsSSCMEnabled: expected bool") + }, + "AllowPrereleaseUpdates": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetAllowPrereleaseUpdates(b) + } + return fmt.Errorf("invalid type for AllowPrereleaseUpdates: expected bool") + }, + "AllowMajorUpdates": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetAllowMajorUpdates(b) + } + return fmt.Errorf("invalid type for AllowMajorUpdates: expected bool") + }, + //"IsCodeServerEnabled": func(v interface{}) error { + // if b, ok := v.(bool); ok { + // return config.SetIsCodeServerEnabled(b) + // } + // return fmt.Errorf("invalid type for IsCodeServerEnabled: expected bool") + //}, + //"BackupContentDir": func(v interface{}) error { + // if str, ok := v.(string); ok { + // return config.SetBackupContentDir(str) + // } + // return fmt.Errorf("invalid type for BackupContentDir: expected string") + //}, + //"BackupsStoreDir": func(v interface{}) error { + // if str, ok := v.(string); ok { + // return config.SetBackupsStoreDir(str) + // } + // return fmt.Errorf("invalid type for BackupsStoreDir: expected string") + //}, + //"BackupLoopInterval": func(v interface{}) error { + // if str, ok := v.(string); ok { + // interval, err := time.ParseDuration(str) + // if err != nil { + // return err + // } + // return config.SetBackupLoopInterval(interval) + // } + // return fmt.Errorf("invalid type for BackupLoopInterval: expected string") + //}, + //"BackupMode": func(v interface{}) error { + // if str, ok := v.(string); ok { + // return config.SetBackupMode(str) + // } + // return fmt.Errorf("invalid type for BackupMode: expected string") + //}, + //"BackupMaxFileSize": func(v interface{}) error { + // if f, ok := v.(float64); ok { + // return config.SetBackupMaxFileSize(int64(f)) + // } + // return fmt.Errorf("invalid type for BackupMaxFileSize: expected number") + //}, + //"BackupUseCompression": func(v interface{}) error { + // if b, ok := v.(bool); ok { + // return config.SetBackupUseCompression(b) + // } + // return fmt.Errorf("invalid type for BackupUseCompression: expected bool") + //}, + //"BackupKeepSnapshot": func(v interface{}) error { + // if b, ok := v.(bool); ok { + // return config.SetBackupKeepSnapshot(b) + // } + // return fmt.Errorf("invalid type for BackupKeepSnapshot: expected bool") + //}, + //"IsTelemetryEnabled": func(v interface{}) error { + // //Accepts both bool and an empty string as a valid value for true. + // var expectedtype string + // if _, ok := v.(string); ok { + // expectedtype = "string" + // return config.SetIsTelemetryEnabled(true) + // } + // if _, ok := v.(bool); ok { + // expectedtype = "bool" + // return config.SetIsTelemetryEnabled(v.(bool)) + // } + // return fmt.Errorf("invalid type for IsTelemetryEnabled: expected %s", expectedtype) + //}, + "IsConsoleEnabled": func(v interface{}) error { + if b, ok := v.(bool); ok { + return config.SetIsConsoleEnabled(b) + } + return fmt.Errorf("invalid type for IsConsoleEnabled: expected bool") + }, +} + +// SaveSetting handles RESTful requests to update a single configuration setting +func SaveSetting(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } + + // Read request body + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, fmt.Sprintf("Error reading request body: %v", err), http.StatusInternalServerError) + return + } + defer r.Body.Close() + + // Parse JSON into a map + var requestData map[string]interface{} + if err := json.Unmarshal(body, &requestData); err != nil { + http.Error(w, fmt.Sprintf("Error parsing JSON: %v", err), http.StatusBadRequest) + return + } + + // Ensure exactly one key-value pair + if len(requestData) != 1 { + http.Error(w, "Request must contain exactly one key-value pair", http.StatusBadRequest) + return + } + + // Get the single key and value + var key string + var value interface{} + for k, v := range requestData { + key = k + value = v + break + } + + // Look up the setter + setter, exists := setterMap[key] + if !exists { + http.Error(w, fmt.Sprintf("Unknown configuration key: %s", key), http.StatusBadRequest) + return + } + + // Call the setter + if err := setter(value); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{ + "status": "error", + "message": err.Error(), + }) + return + } + + // Success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "status": "success", + "message": "Configuration updated successfully", + }) +} diff --git a/src/web/routes.go b/src/web/routes.go index 9c4f02a3..c24f40df 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -8,6 +8,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config/configchanger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/settings" ) func SetupRoutes() (*http.ServeMux, *http.ServeMux) { @@ -88,6 +89,8 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/runfile/save", HandleRunfileSave) protectedMux.HandleFunc("/api/v2/runfile/hardreset", HandleSetRunfileGame) protectedMux.HandleFunc("/api/v2/loader/reloadrunfile", HandleReloadRunfile) + protectedMux.HandleFunc("/api/v2/settings/save", settings.SaveSetting) + protectedMux.HandleFunc("/api/v2/settings", settings.RetrieveSettings) return mux, protectedMux } From fa0517757875d0f0c2d479e415d0edaf1435c76f Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 22:31:53 +0200 Subject: [PATCH 03/93] added config interfaces from SteamServerUI --- .vscode/launch.json | 11 + frontend/package.json | 2 +- .../components/settings/AppSettings.svelte | 505 ++++++++++++++++++ .../settings/BackendSettings.svelte | 452 ++++++++++++++++ .../settings/RunfileSettings.svelte | 494 +++++++++++++++++ .../components/settings/SettingsView.svelte | 33 +- 6 files changed, 1492 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/settings/AppSettings.svelte create mode 100644 frontend/src/components/settings/BackendSettings.svelte create mode 100644 frontend/src/components/settings/RunfileSettings.svelte diff --git a/.vscode/launch.json b/.vscode/launch.json index b5d9d441..2498df4a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,6 +29,17 @@ "console": "integratedTerminal", "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm "args": ["--NoSteamCMD"] + }, + { + "name": "Debug Go Server noSteamCMD BUT svelte", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/server.go", + "console": "integratedTerminal", + "preLaunchTask": "npm: build", + "showLog": false, // Hides some Go Debugger(Delve) log stuff that is not useful for debugging atm + "args": ["--NoSteamCMD"] } ] } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 7abeceef..eb5f3888 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "steamserverui", "main": "main.cjs", "private": true, - "version": "v5.6.4", + "version": "v5.7.1", "description": "Svelte UI Interface for Steam Server UI (SSUI) Backend", "author": { "name": "JacksonTheMaster", diff --git a/frontend/src/components/settings/AppSettings.svelte b/frontend/src/components/settings/AppSettings.svelte new file mode 100644 index 00000000..2c120bbd --- /dev/null +++ b/frontend/src/components/settings/AppSettings.svelte @@ -0,0 +1,505 @@ + + + + {#if activeSidebarTab === 'General' || activeSidebarTab === 'SSUI Settings'} +
+

SSUI Settings

+ +

+ Configure general application settings. Changes will be applied immediately. +

+ + {#if settingsGroups.length > 0} +
+ {#each settingsGroups as group} + + {/each} +
+ + {#each settingsGroups as group} + {#if activeSettingsGroup === group} +
+

{group}

+
+ {#each settingsData.filter(s => s.group === group) as setting} +
+
+

{setting.name}

+ {#if setting.description} +

{setting.description}

+ {/if} +
+ +
+ {#if setting.type === 'bool'} + + {:else if setting.type === 'int'} + handleInputChange(setting, e)} + class="number-input" + /> + {:else if setting.type === 'array'} + + {:else if setting.type === 'map'} + + {:else} + handleInputChange(setting, e)} + class="text-input" + /> + {/if} +
+
+ {/each} +
+
+ {/if} + {/each} + + {#if statusMessage} +
+ {isError ? '⚠️' : '✓'} + {statusMessage} + +
+ {/if} + {:else} +
+
+

Loading settings...

+
+ {/if} +
+{/if} + + \ No newline at end of file diff --git a/frontend/src/components/settings/BackendSettings.svelte b/frontend/src/components/settings/BackendSettings.svelte new file mode 100644 index 00000000..36354a92 --- /dev/null +++ b/frontend/src/components/settings/BackendSettings.svelte @@ -0,0 +1,452 @@ + + + +
+
+

Backend Connections

+
+ +
+
+

Configured Backends

+
+ {#if activeBackendStatus.status === 'online'} + 🟢 Online + {:else if activeBackendStatus.status === 'offline'} + 🔴 Offline + {:else if activeBackendStatus.status === 'error'} + ⚠️ Error + {:else} + ⚪ Unknown + {/if} + {#if activeBackendStatus.lastChecked} + + (Last checked: {new Date(activeBackendStatus.lastChecked).toLocaleTimeString()}) + + {/if} +
+ +
+ +
+ {#each backends as backendId} +
+
+

{backendId}

+

{currentConfig.backends[backendId].url}

+
+
+ +
+
+ {#if backendId !== 'default'} + + {/if} +
+
+ {/each} +
+
+ +
+
+

Add New Backend

+
+ +
+
+ + +
+ +
+ + +
+ + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/components/settings/RunfileSettings.svelte b/frontend/src/components/settings/RunfileSettings.svelte new file mode 100644 index 00000000..8adcc00b --- /dev/null +++ b/frontend/src/components/settings/RunfileSettings.svelte @@ -0,0 +1,494 @@ + + + +
+

Game Settings

+ +

+ Configure command line arguments that will be used when launching the game server. +

+ + {#if isLoading && runfileGroups.length === 0} +
+
+

Loading runfile configuration...

+
+ {:else if runfileGroups.length === 0} +
+

No Runfile Groups Available

+

Select a runfile from the Runfile Gallery to get started!

+
+ {:else} +
+
+ {#each runfileGroups as group} + + {/each} +
+ + {#if isLoading && activeRunfileGroup} +
+
+

Loading {activeRunfileGroup} settings...

+
+ {:else if activeRunfileGroup} +
+

{activeRunfileGroup} GameServer Settings

+ + {#if runfileArgs.length === 0} +
+

No configurable arguments found for this group.

+
+ {:else} +
+ {#each runfileArgs as arg} +
+ +
{arg.description || 'No description available'}
+
+ {/each} +
+ +
+ +
+ {/if} +
+ {/if} + + {#if statusMessage} +
+ {isError ? '⚠️' : '✓'} + {statusMessage} + +
+ {/if} +
+ {/if} +
+ + \ No newline at end of file diff --git a/frontend/src/components/settings/SettingsView.svelte b/frontend/src/components/settings/SettingsView.svelte index 86b93202..a327f474 100644 --- a/frontend/src/components/settings/SettingsView.svelte +++ b/frontend/src/components/settings/SettingsView.svelte @@ -1,7 +1,12 @@
+ diff --git a/frontend/src/components/Dashboard/cards/SystemInfoCard.svelte b/frontend/src/components/Dashboard/cards/SystemInfoCard.svelte new file mode 100644 index 00000000..e5e8cd02 --- /dev/null +++ b/frontend/src/components/Dashboard/cards/SystemInfoCard.svelte @@ -0,0 +1,220 @@ + + +
+
+

System Information

+
ℹ️
+
+ {#if error} +
{error}
+ + {:else if systemInfo} +
+
+
{typeof systemInfo.cpuUsage === 'number' ? systemInfo.cpuUsage.toFixed(2) : 'N/A'}%
+
System CPU Usage
+
+
+
+
{typeof systemInfo.memoryUsage === 'number' ? (systemInfo.memoryUsage/1024).toFixed(2) : 'N/A'} GB
+
Memory Consumption
+
+
+
+
{typeof systemInfo.diskUsage === 'number' ? (100-systemInfo.diskUsage).toFixed(2) : 'N/A'}%
+
Disk Space Unused
+
+
+
+
+
+
OS Version
+
{systemInfo.osName || 'Temple OS v3.14.15'} {systemInfo.osVersion || ''}
+
+
+
Kernel
+
{systemInfo.kernel || 'Cannot detect'}
+
+
+
Uptime
+
{systemInfo.uptime || 'Cannot detect'}
+
+
+
Backend IP Address
+
{systemInfo.backendIpAddress || 'Cannot detect'}
+
+
+
+
+
Last Refresh
+
{systemInfo.lastRefreshTime ? new Date(systemInfo.lastRefreshTime).toLocaleString() : 'Cannot detect'}
+
+
+ {:else} +
Loading system information...
+ {/if} +
+ + \ No newline at end of file diff --git a/src/steamserverui/systeminfo/systeminfo.go b/src/steamserverui/systeminfo/systeminfo.go new file mode 100644 index 00000000..19395d75 --- /dev/null +++ b/src/steamserverui/systeminfo/systeminfo.go @@ -0,0 +1,403 @@ +package systeminfo + +import ( + "fmt" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + "time" +) + +// OsStats represents the OS statistics that will be returned to the client +type OsStats struct { + OSName string `json:"osName"` + OSVersion string `json:"osVersion"` + Kernel string `json:"kernel"` + Uptime string `json:"uptime"` + BackendIPAddr string `json:"backendIpAddress"` + CPUUsage float64 `json:"cpuUsage"` + MemoryUsage float64 `json:"memoryUsage"` + DiskUsage float64 `json:"diskUsage"` + LastRefreshTime string `json:"lastRefreshTime"` +} + +var ( + osStatsMutex sync.RWMutex + cachedOsStats *OsStats + lastRefreshTime time.Time + // Set cache duration to 1 minute + cacheDuration = 60 * time.Second +) + +// CPUStats holds CPU time statistics +type CPUStats struct { + idle uint64 + total uint64 +} + +// refreshCachedStats updates the OS stats in the cache +func RefreshCachedStats() (*OsStats, error) { + osStatsMutex.Lock() + defer osStatsMutex.Unlock() + + // Check if cache is still valid + if cachedOsStats != nil && time.Since(lastRefreshTime) < cacheDuration { + return cachedOsStats, nil + } + + // Gather all OS statistics + stats := &OsStats{} + + // Get OS name and version + switch runtime.GOOS { + case "windows": + stats.OSName = "Windows" + cmd := exec.Command("powershell", "-Command", "(Get-WmiObject -class Win32_OperatingSystem).Caption") + output, err := cmd.Output() + if err == nil { + stats.OSVersion = strings.TrimSpace(string(output)) + } else { + stats.OSVersion = "Unknown Windows Version" + } + case "linux": + // Try to get OS name from /etc/os-release + if data, err := os.ReadFile("/etc/os-release"); err == nil { + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "NAME=") { + stats.OSName = strings.Trim(strings.TrimPrefix(line, "NAME="), "\"") + } else if strings.HasPrefix(line, "VERSION=") { + stats.OSVersion = strings.Trim(strings.TrimPrefix(line, "VERSION="), "\"") + } + } + } + + if stats.OSName == "" { + stats.OSName = "Linux" + } + + if stats.OSVersion == "" { + // Try lsb_release if available + cmd := exec.Command("lsb_release", "-d") + output, err := cmd.Output() + if err == nil { + stats.OSVersion = strings.TrimSpace(strings.TrimPrefix(string(output), "Description:")) + } else { + stats.OSVersion = "Unknown Linux Version" + } + } + default: + stats.OSName = runtime.GOOS + stats.OSVersion = "Unknown Version" + } + + // Get kernel version + switch runtime.GOOS { + case "windows": + cmd := exec.Command("powershell", "-Command", "(Get-WmiObject Win32_OperatingSystem).Version") + output, err := cmd.Output() + if err == nil { + stats.Kernel = strings.TrimSpace(string(output)) + } else { + stats.Kernel = "Unknown" + } + case "linux": + cmd := exec.Command("uname", "-r") + output, err := cmd.Output() + if err == nil { + stats.Kernel = strings.TrimSpace(string(output)) + } else { + stats.Kernel = "Unknown" + } + default: + stats.Kernel = "Unknown" + } + + // Get uptime + switch runtime.GOOS { + case "windows": + cmd := exec.Command("powershell", "-Command", "(Get-WmiObject Win32_OperatingSystem).LastBootUpTime") + output, err := cmd.Output() + if err == nil { + bootTime := strings.TrimSpace(string(output)) + bootTimeFormat := "20060102150405.000000-070" + t, err := time.Parse(bootTimeFormat, bootTime) + if err == nil { + uptime := time.Since(t) + days := int(uptime.Hours() / 24) + hours := int(uptime.Hours()) % 24 + mins := int(uptime.Minutes()) % 60 + + if days > 0 { + stats.Uptime = fmt.Sprintf("%d days, %d hours, %d minutes", days, hours, mins) + } else if hours > 0 { + stats.Uptime = fmt.Sprintf("%d hours, %d minutes", hours, mins) + } else { + stats.Uptime = fmt.Sprintf("%d minutes", mins) + } + } else { + stats.Uptime = "Unknown" + } + } else { + stats.Uptime = "Unknown" + } + case "linux": + if data, err := os.ReadFile("/proc/uptime"); err == nil { + fields := strings.Fields(string(data)) + if len(fields) > 0 { + uptimeSec, err := strconv.ParseFloat(fields[0], 64) + if err == nil { + uptime := time.Duration(uptimeSec) * time.Second + days := int(uptime.Hours() / 24) + hours := int(uptime.Hours()) % 24 + mins := int(uptime.Minutes()) % 60 + + if days > 0 { + stats.Uptime = fmt.Sprintf("%d days, %d hours, %d minutes", days, hours, mins) + } else if hours > 0 { + stats.Uptime = fmt.Sprintf("%d hours, %d minutes", hours, mins) + } else { + stats.Uptime = fmt.Sprintf("%d minutes", mins) + } + } else { + stats.Uptime = "Unknown" + } + } else { + stats.Uptime = "Unknown" + } + } else { + stats.Uptime = "Unknown" + } + default: + stats.Uptime = "Unknown" + } + + // Get backend IP address - try to get a non-loopback IP + stats.BackendIPAddr = getIPAddress() + + // Get CPU usage + stats.CPUUsage = getCPUUsage() + + // Get memory usage + stats.MemoryUsage = getMemoryUsage() + + // Get disk usage + stats.DiskUsage = getDiskUsage() + + // Update timestamp + now := time.Now() + stats.LastRefreshTime = now.Format(time.RFC3339) + lastRefreshTime = now + cachedOsStats = stats + + return stats, nil +} + +// getIPAddress returns a non-loopback IP address of the machine +func getIPAddress() string { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("powershell", "-Command", "Get-NetIPAddress | Where-Object {$_.AddressFamily -eq 'IPv4' -and $_.IPAddress -ne '127.0.0.1'} | Select-Object -ExpandProperty IPAddress -First 1") + output, err := cmd.Output() + if err == nil && len(output) > 0 { + return strings.TrimSpace(string(output)) + } + case "linux": + cmd := exec.Command("hostname", "-I") + output, err := cmd.Output() + if err == nil && len(output) > 0 { + ips := strings.Fields(string(output)) + if len(ips) > 0 { + return ips[0] + } + } + } + return "127.0.0.1" // Default to loopback if we can't find anything else +} + +// getCPUUsage returns the CPU usage as a percentage +func getCPUUsage() float64 { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("powershell", "-Command", `(1..5 | ForEach-Object { (Get-CimInstance Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'").PercentProcessorTime; Start-Sleep 1 } | Measure-Object -Average).Average`) + output, err := cmd.Output() + if err == nil { + if usage, err := strconv.ParseFloat(strings.TrimSpace(string(output)), 64); err == nil { + return usage + } + } + case "linux": + // First try using /proc/stat for accurate measurement across environments including containers + // We need to sample twice with a small delay to calculate CPU usage + prev, err := readCPUStats() + if err == nil { + // Small sleep to get a delta + time.Sleep(200 * time.Millisecond) + current, err := readCPUStats() + if err == nil { + // Calculate the delta between measurements + idleDelta := current.idle - prev.idle + totalDelta := current.total - prev.total + + if totalDelta > 0 { + // CPU usage is the percentage of non-idle time + return 100.0 * (1.0 - float64(idleDelta)/float64(totalDelta)) + } + } + } + + // Fallback to other methods if /proc/stat didn't work + methods := []func() (float64, error){ + // Method 1: Using top + func() (float64, error) { + cmd := exec.Command("sh", "-c", "top -bn1 | grep 'Cpu(s)' | awk '{print $2 + $4}'") + output, err := cmd.Output() + if err != nil { + return 0.0, err + } + return strconv.ParseFloat(strings.TrimSpace(string(output)), 64) + }, + // Method 2: Using mpstat if available + func() (float64, error) { + cmd := exec.Command("sh", "-c", "command -v mpstat >/dev/null 2>&1 && mpstat 1 1 | awk '/Average:/ && $12 ~ /[0-9.]+/ {print 100 - $12}'") + output, err := cmd.Output() + if err != nil { + return 0.0, err + } + return strconv.ParseFloat(strings.TrimSpace(string(output)), 64) + }, + // Method 3: Using /proc/loadavg as a rough indicator + func() (float64, error) { + data, err := os.ReadFile("/proc/loadavg") + if err != nil { + return 0.0, err + } + fields := strings.Fields(string(data)) + if len(fields) < 1 { + return 0.0, fmt.Errorf("invalid format in /proc/loadavg") + } + loadavg, err := strconv.ParseFloat(fields[0], 64) + if err != nil { + return 0.0, err + } + // Get number of CPUs to normalize the load + var numCPU int + if cpuinfo, err := os.ReadFile("/proc/cpuinfo"); err == nil { + processors := strings.Count(string(cpuinfo), "processor") + if processors > 0 { + numCPU = processors + } else { + numCPU = runtime.NumCPU() + } + } else { + numCPU = runtime.NumCPU() + } + // Convert load average to a percentage (capped at 100%) + cpuUsage := (loadavg / float64(numCPU)) * 100 + if cpuUsage > 100 { + cpuUsage = 100 + } + return cpuUsage, nil + }, + } + + // Try each method until one works + for _, method := range methods { + if usage, err := method(); err == nil { + return usage + } + } + } + return 0.0 // Default to 0 if we can't determine the CPU usage +} + +// readCPUStats reads the current CPU statistics from /proc/stat +func readCPUStats() (CPUStats, error) { + data, err := os.ReadFile("/proc/stat") + if err != nil { + return CPUStats{}, err + } + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 5 && fields[0] == "cpu" { + // CPU line format: cpu user nice system idle iowait irq softirq steal guest guest_nice + // We need to sum all fields except idle (field 4) to get total CPU time + var total uint64 + idle, _ := strconv.ParseUint(fields[4], 10, 64) + + for i := 1; i < len(fields); i++ { + val, _ := strconv.ParseUint(fields[i], 10, 64) + total += val + } + + return CPUStats{idle: idle, total: total}, nil + } + } + + return CPUStats{}, fmt.Errorf("failed to parse CPU stats from /proc/stat") +} + +// getMemoryUsage returns the systems memory usage in MB +func getMemoryUsage() float64 { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("powershell", "-Command", "[math]::Round((((Get-WmiObject Win32_OperatingSystem).TotalVisibleMemorySize - (Get-WmiObject Win32_OperatingSystem).FreePhysicalMemory) /1024 ) )") + output, err := cmd.Output() + if err == nil { + if usage, err := strconv.ParseFloat(strings.TrimSpace(string(output)), 64); err == nil { + return usage + } + } + case "linux": + cmd := exec.Command("sh", "-c", "free | grep Mem | awk '{print ($3) / 1024}'") + output, err := cmd.Output() + if err == nil { + if usage, err := strconv.ParseFloat(strings.TrimSpace(string(output)), 64); err == nil { + return usage + } + } + } + return 0.0 // Default to 0 if we can't determine the memory usage +} + +// getDiskUsage returns the disk usage as a percentage +func getDiskUsage() float64 { + var path string + if runtime.GOOS == "windows" { + path = "C:\\" + } else { + path = "/" + } + + switch runtime.GOOS { + case "windows": + cmd := exec.Command("powershell", "-Command", `[math]::Round(((Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$((Get-Location).Drive.Name):'").Size - (Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$((Get-Location).Drive.Name):'").FreeSpace) / (Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$((Get-Location).Drive.Name):'").Size * 100, 2)`) + output, err := cmd.Output() + if err == nil { + if usage, err := strconv.ParseFloat(strings.TrimSpace(string(output)), 64); err == nil { + return usage + } + } + case "linux": + cmd := exec.Command("df", "-h", path) + output, err := cmd.Output() + if err == nil { + lines := strings.Split(string(output), "\n") + if len(lines) >= 2 { + fields := strings.Fields(lines[1]) + if len(fields) >= 5 { + usage := strings.TrimSuffix(fields[4], "%") + if parsed, err := strconv.ParseFloat(usage, 64); err == nil { + return parsed + } + } + } + } + } + return 0.0 // Default to 0 if we can't determine the disk usage +} diff --git a/src/web/routes.go b/src/web/routes.go index c24f40df..6b48b533 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -82,15 +82,20 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { // SteamServerUI + // --- RUNFILE --- protectedMux.HandleFunc("/api/v2/runfile/groups", HandleRunfileGroups) protectedMux.HandleFunc("/api/v2/runfile/args", HandleRunfileArgs) protectedMux.HandleFunc("/api/v2/runfile/args/update", HandleRunfileArgUpdate) protectedMux.HandleFunc("/api/v2/runfile", HandleRunfile) protectedMux.HandleFunc("/api/v2/runfile/save", HandleRunfileSave) protectedMux.HandleFunc("/api/v2/runfile/hardreset", HandleSetRunfileGame) + // --- LOADER --- protectedMux.HandleFunc("/api/v2/loader/reloadrunfile", HandleReloadRunfile) + // --- SETTINGS --- protectedMux.HandleFunc("/api/v2/settings/save", settings.SaveSetting) protectedMux.HandleFunc("/api/v2/settings", settings.RetrieveSettings) + // --- OS STATS --- + protectedMux.HandleFunc("/api/v2/osstats", HandleGetOsStats) return mux, protectedMux } diff --git a/src/web/systeminfo.go b/src/web/systeminfo.go new file mode 100644 index 00000000..ccb52e99 --- /dev/null +++ b/src/web/systeminfo.go @@ -0,0 +1,35 @@ +package web + +import ( + "encoding/json" + "net/http" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/systeminfo" +) + +func HandleGetOsStats(w http.ResponseWriter, r *http.Request) { + logger.Web.Debug("Received getOsStats request from API") + // accept only GET requests + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) + return + } + + // Get cached stats or refresh if needed + stats, err := systeminfo.RefreshCachedStats() + if err != nil { + logger.Web.Error("Failed to get OS stats") + http.Error(w, "Failed to get OS stats", http.StatusInternalServerError) + return + } + + // Set response headers and write JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(stats); err != nil { + logger.Web.Error("Failed to write response") + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } +} From d24ff6170386f599a8a276ee03e38c07f446cd31 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Tue, 30 Sep 2025 23:12:03 +0200 Subject: [PATCH 05/93] added runfile gallery from SteamServerUI --- frontend/src/App.svelte | 1 + .../src/components/Gallery/ErrorPopup.svelte | 93 +++++ .../src/components/Gallery/RunfileCard.svelte | 311 ++++++++++++++++ .../components/Gallery/SteamCMDWait.svelte | 76 ++++ frontend/src/components/MainContent.svelte | 9 + .../views/RunfileGalleryView.svelte | 349 ++++++++++++++++++ src/config/getters.go | 6 + src/steamserverui/runfilegallery/gallery.go | 152 ++++++++ src/web/routes.go | 3 + src/web/runfilegallery.go | 71 ++++ 10 files changed, 1071 insertions(+) create mode 100644 frontend/src/components/Gallery/ErrorPopup.svelte create mode 100644 frontend/src/components/Gallery/RunfileCard.svelte create mode 100644 frontend/src/components/Gallery/SteamCMDWait.svelte create mode 100644 frontend/src/components/views/RunfileGalleryView.svelte create mode 100644 src/steamserverui/runfilegallery/gallery.go create mode 100644 src/web/runfilegallery.go diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 298bfb25..a9178b21 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -16,6 +16,7 @@ { id: 'settings', name: 'Settings', icon: 'settings' }, { id: 'console', name: 'Console', icon: 'terminal' }, { id: 'logs', name: 'Logs', icon: 'file-text' }, + { id: 'gallery', name: 'Gallery', icon: 'globe' }, ]; // Set active view function diff --git a/frontend/src/components/Gallery/ErrorPopup.svelte b/frontend/src/components/Gallery/ErrorPopup.svelte new file mode 100644 index 00000000..69419a5a --- /dev/null +++ b/frontend/src/components/Gallery/ErrorPopup.svelte @@ -0,0 +1,93 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Gallery/RunfileCard.svelte b/frontend/src/components/Gallery/RunfileCard.svelte new file mode 100644 index 00000000..7e3c0b74 --- /dev/null +++ b/frontend/src/components/Gallery/RunfileCard.svelte @@ -0,0 +1,311 @@ + + + +
+
e.key === 'Enter'} tabindex="0" role="button" aria-label={`Runfile card for ${runfile.name}`}> + +
+
+ + +
+

{runfile.name}

+ +
+ + +
+
+ {#if isLoading} + {#if isSteamCMDRunning} + + {:else} +
+ {/if} + {:else} + + {/if} +
+ + {#if showErrorPopup} + + {/if} +
+
+
+
+
+ + \ No newline at end of file diff --git a/frontend/src/components/Gallery/SteamCMDWait.svelte b/frontend/src/components/Gallery/SteamCMDWait.svelte new file mode 100644 index 00000000..484a1a15 --- /dev/null +++ b/frontend/src/components/Gallery/SteamCMDWait.svelte @@ -0,0 +1,76 @@ + + +
+
+

{currentMessage}

+

Please don't poke the backend!

+
+ + \ No newline at end of file diff --git a/frontend/src/components/MainContent.svelte b/frontend/src/components/MainContent.svelte index 482c074c..c1550d1d 100644 --- a/frontend/src/components/MainContent.svelte +++ b/frontend/src/components/MainContent.svelte @@ -4,6 +4,7 @@ import SettingsView from './settings/SettingsView.svelte'; import LogsView from './views/LogsView.svelte'; import ConsoleView from './views/ConsoleView.svelte'; + import RunfileGalleryView from './views/RunfileGalleryView.svelte'; /** * @typedef {Object} Props @@ -35,6 +36,10 @@ title: 'Console', description: 'View server console output' }, + gallery: { + title: 'Runfile Gallery', + description: 'Browse runfiles' + }, }; @@ -61,6 +66,10 @@
+ {:else if activeView === 'gallery'} +
+ +
{/if}
diff --git a/frontend/src/components/views/RunfileGalleryView.svelte b/frontend/src/components/views/RunfileGalleryView.svelte new file mode 100644 index 00000000..653ae3c9 --- /dev/null +++ b/frontend/src/components/views/RunfileGalleryView.svelte @@ -0,0 +1,349 @@ + + + + + \ No newline at end of file diff --git a/src/config/getters.go b/src/config/getters.go index 27067b83..090fdff5 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -513,3 +513,9 @@ func GetIsDockerContainer() bool { defer ConfigMu.RUnlock() return IsDockerContainer } + +func GetRunfilesFolder() string { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return RunFilesFolder +} diff --git a/src/steamserverui/runfilegallery/gallery.go b/src/steamserverui/runfilegallery/gallery.go new file mode 100644 index 00000000..cad7fed6 --- /dev/null +++ b/src/steamserverui/runfilegallery/gallery.go @@ -0,0 +1,152 @@ +package runfilegallery + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// GalleryRunfile represents a runfile in the gallery +type GalleryRunfile struct { + Name string `json:"name"` + Filename string `json:"filename"` + Version string `json:"version"` + BackgroundURL string `json:"background_url"` + LogoURL string `json:"logo_url"` + SupportedOS string `json:"supported_os"` + MinVersion string `json:"min_version"` +} + +// galleryCache stores the parsed and filtered runfile list +var ( + galleryCache []GalleryRunfile + cacheMutex sync.Mutex +) + +// GetRunfileGallery fetches the list of available runfiles from GitHub Pages +func GetRunfileGallery(forceUpdate bool) ([]GalleryRunfile, error) { + cacheMutex.Lock() + defer cacheMutex.Unlock() + + // Return cached results if not forcing an update and cache is populated + if !forceUpdate && len(galleryCache) > 0 { + logger.Runfile.Debug("Serving runfile gallery from cache") + return galleryCache, nil + } + + // Fetch manifest from GitHub Pages + const manifestURL = "https://steamserverui.github.io/runfiles/manifest.ssui" + logger.Runfile.Info("Fetching runfile gallery from " + manifestURL) + resp, err := http.Get(manifestURL) + if err != nil { + logger.Runfile.Error(fmt.Sprintf("Failed to fetch manifest: %v", err)) + return nil, fmt.Errorf("couldn't reach the gallery, network's playing hide and seek") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Runfile.Error(fmt.Sprintf("Manifest fetch failed with status: %d", resp.StatusCode)) + return nil, fmt.Errorf("gallery failed to fetch manifest from GitHub, status: %d", resp.StatusCode) + } + + // Parse manifest + var runfiles []GalleryRunfile + if err := json.NewDecoder(resp.Body).Decode(&runfiles); err != nil { + logger.Runfile.Error(fmt.Sprintf("Failed to parse manifest: %v", err)) + return nil, fmt.Errorf("manifest is gibberish, can't make sense of it") + } + + // Filter by backend version + currentVersion := config.GetVersion() + var filtered []GalleryRunfile + for _, rf := range runfiles { + if compareVersions(rf.MinVersion, currentVersion) <= 0 { + filtered = append(filtered, rf) + } else { + logger.Runfile.Debug(fmt.Sprintf("Skipping runfile %s, requires version %s, current is %s", rf.Name, rf.MinVersion, currentVersion)) + } + } + + // Update cache + galleryCache = filtered + logger.Runfile.Info(fmt.Sprintf("Fetched and cached %d runfiles", len(filtered))) + + if len(filtered) == 0 { + logger.Runfile.Warn("No runfiles compatible with backend version " + currentVersion) + } + + return filtered, nil +} + +// saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir +func SaveRunfileToDisk(identifier string) error { + filename := fmt.Sprintf("run%s.ssui", identifier) + baseURL := "https://steamserverui.github.io/runfiles" + fileURL := fmt.Sprintf("%s/%s", baseURL, filename) + + logger.Runfile.Info("Fetching runfile from " + fileURL) + resp, err := http.Get(fileURL) + if err != nil { + logger.Runfile.Error(fmt.Sprintf("Failed to fetch runfile %s: %v", filename, err)) + return fmt.Errorf("couldn't grab %s, network's being a jerk", filename) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Runfile.Error(fmt.Sprintf("Runfile %s fetch failed with status: %d", filename, resp.StatusCode)) + return fmt.Errorf("%s is playing hard to get, status: %d", filename, resp.StatusCode) + } + + saveFilePath := filepath.Join(config.GetRunfilesFolder(), filename) + logger.Runfile.Info("Saving runfile to " + saveFilePath) + + // Create or overwrite the file + file, err := os.Create(saveFilePath) + if err != nil { + logger.Runfile.Error(fmt.Sprintf("Failed to create file %s: %v", saveFilePath, err)) + return fmt.Errorf("disk's throwing a fit, can't save file") + } + defer file.Close() + + // Copy response body to file + if _, err := io.Copy(file, resp.Body); err != nil { + logger.Runfile.Error(fmt.Sprintf("Failed to save runfile %s: %v", filename, err)) + return fmt.Errorf("couldn't save %s, disk's being dramatic", filename) + } + + logger.Runfile.Info("Successfully saved runfile " + filename) + return nil +} + +// compareVersions compares two semantic version strings (x.y.z) +// Returns -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 +func compareVersions(v1, v2 string) int { + v1Parts := strings.Split(v1, ".") + v2Parts := strings.Split(v2, ".") + + // Ensure both versions have 3 parts + for i := 0; i < 3; i++ { + var n1, n2 int + if i < len(v1Parts) { + fmt.Sscanf(v1Parts[i], "%d", &n1) + } + if i < len(v2Parts) { + fmt.Sscanf(v2Parts[i], "%d", &n2) + } + if n1 < n2 { + return -1 + } + if n1 > n2 { + return 1 + } + } + return 0 +} diff --git a/src/web/routes.go b/src/web/routes.go index 6b48b533..7cc42aa1 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -96,6 +96,9 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/settings", settings.RetrieveSettings) // --- OS STATS --- protectedMux.HandleFunc("/api/v2/osstats", HandleGetOsStats) + // --- RUNFILE GALLERY --- + protectedMux.HandleFunc("/api/v2/gallery", galleryHandler) + protectedMux.HandleFunc("/api/v2/gallery/select", selectHandler) return mux, protectedMux } diff --git a/src/web/runfilegallery.go b/src/web/runfilegallery.go new file mode 100644 index 00000000..68fffccc --- /dev/null +++ b/src/web/runfilegallery.go @@ -0,0 +1,71 @@ +package web + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfilegallery" +) + +// response wraps API responses +type response struct { + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +// galleryHandler handles GET /api/v2/gallery +func galleryHandler(w http.ResponseWriter, r *http.Request) { + logger.Runfile.Info("Handling GET /api/v2/gallery request") + forceUpdate := strings.ToLower(r.URL.Query().Get("forceUpdate")) == "true" + + runfiles, err := runfilegallery.GetRunfileGallery(forceUpdate) + if err != nil { + logger.Runfile.Error("Gallery fetch failed: " + err.Error()) + sendResponse(w, http.StatusInternalServerError, response{Error: err.Error()}) + return + } + + logger.Runfile.Info("Returning " + strconv.Itoa(len(runfiles)) + " runfiles from gallery") + sendResponse(w, http.StatusOK, response{Data: runfiles}) +} + +// selectHandler handles POST /api/v2/gallery/select +func selectHandler(w http.ResponseWriter, r *http.Request) { + logger.Runfile.Info("Handling POST /api/v2/gallery/select request") + + var req struct { + Identifier string `json:"identifier"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.Runfile.Error("Invalid request body: " + err.Error()) + sendResponse(w, http.StatusBadRequest, response{Error: "invalid JSON, check your request"}) + return + } + + if req.Identifier == "" { + logger.Runfile.Error("Missing identifier in request") + sendResponse(w, http.StatusBadRequest, response{Error: "identifier is required"}) + return + } + + if err := runfilegallery.SaveRunfileToDisk(req.Identifier); err != nil { + logger.Runfile.Error("Failed to save runfile " + req.Identifier + ": " + err.Error()) + sendResponse(w, http.StatusInternalServerError, response{Error: err.Error()}) + return + } + + logger.Runfile.Info("Successfully saved runfile " + req.Identifier) + sendResponse(w, http.StatusOK, response{Data: "Runfile " + req.Identifier + " saved"}) +} + +// sendResponse writes a JSON response with the given status code +func sendResponse(w http.ResponseWriter, status int, resp response) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(resp); err != nil { + logger.Runfile.Error("Failed to encode response: " + err.Error()) + } +} From 3e20c251e84cd2b2fcf9c23ed4d89272cded612e Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:04:10 +0200 Subject: [PATCH 06/93] reorganized package names --- src/core/loader/runfile.go | 4 +- .../{runfilegallery => gallery}/gallery.go | 2 +- .../{runfilemanager => runfile}/argexample.go | 2 +- .../{runfilemanager => runfile}/args.go | 2 +- .../{runfilemanager => runfile}/getters.go | 2 +- src/web/runfile.go | 38 +++++++++---------- src/web/runfilegallery.go | 6 +-- 7 files changed, 28 insertions(+), 28 deletions(-) rename src/steamserverui/{runfilegallery => gallery}/gallery.go (99%) rename src/steamserverui/{runfilemanager => runfile}/argexample.go (97%) rename src/steamserverui/{runfilemanager => runfile}/args.go (99%) rename src/steamserverui/{runfilemanager => runfile}/getters.go (99%) diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 32a21c9d..1393c764 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -7,7 +7,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfilemanager" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) // used via Runfile Gallery @@ -37,7 +37,7 @@ func InitRunfile(game string) error { // used to only reload runfile into memory. Can be triggered from v1 UI -> Runfile Reset terminal func ReloadRunfile() error { - if err := runfilemanager.LoadRunfile(config.GetRunfileGame(), config.GetRunFilesFolder()); err != nil { + if err := runfile.LoadRunfile(config.GetRunfileGame(), config.GetRunFilesFolder()); err != nil { logger.Runfile.Warn("Failed to reload runfile: " + err.Error()) return err } diff --git a/src/steamserverui/runfilegallery/gallery.go b/src/steamserverui/gallery/gallery.go similarity index 99% rename from src/steamserverui/runfilegallery/gallery.go rename to src/steamserverui/gallery/gallery.go index cad7fed6..00db95ec 100644 --- a/src/steamserverui/runfilegallery/gallery.go +++ b/src/steamserverui/gallery/gallery.go @@ -1,4 +1,4 @@ -package runfilegallery +package gallery import ( "encoding/json" diff --git a/src/steamserverui/runfilemanager/argexample.go b/src/steamserverui/runfile/argexample.go similarity index 97% rename from src/steamserverui/runfilemanager/argexample.go rename to src/steamserverui/runfile/argexample.go index 63279371..23d43d57 100644 --- a/src/steamserverui/runfilemanager/argexample.go +++ b/src/steamserverui/runfile/argexample.go @@ -1,4 +1,4 @@ -package runfilemanager +package runfile import ( "fmt" diff --git a/src/steamserverui/runfilemanager/args.go b/src/steamserverui/runfile/args.go similarity index 99% rename from src/steamserverui/runfilemanager/args.go rename to src/steamserverui/runfile/args.go index 275f8861..44d8604a 100644 --- a/src/steamserverui/runfilemanager/args.go +++ b/src/steamserverui/runfile/args.go @@ -1,4 +1,4 @@ -package runfilemanager +package runfile import ( "encoding/json" diff --git a/src/steamserverui/runfilemanager/getters.go b/src/steamserverui/runfile/getters.go similarity index 99% rename from src/steamserverui/runfilemanager/getters.go rename to src/steamserverui/runfile/getters.go index 6fabab6d..c56075fc 100644 --- a/src/steamserverui/runfilemanager/getters.go +++ b/src/steamserverui/runfile/getters.go @@ -1,4 +1,4 @@ -package runfilemanager +package runfile import ( "fmt" diff --git a/src/web/runfile.go b/src/web/runfile.go index 3c7eea67..87a1f22a 100644 --- a/src/web/runfile.go +++ b/src/web/runfile.go @@ -8,7 +8,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfilemanager" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) // APIGameArg is a DTO for GameArg, including RuntimeValue and all fields @@ -29,7 +29,7 @@ type APIGameArg struct { Disabled bool `json:"disabled"` } -// APIMeta mirrors runfilemanager.Meta for API responses +// APIMeta mirrors runfile.Meta for API responses type APIMeta struct { Name string `json:"name"` Version string `json:"version"` @@ -51,8 +51,8 @@ type apiResponse struct { Error string `json:"error,omitempty"` } -// toAPIGameArg converts runfilemanager.GameArg to APIGameArg -func toAPIGameArg(arg runfilemanager.GameArg) APIGameArg { +// toAPIGameArg converts runfile.GameArg to APIGameArg +func toAPIGameArg(arg runfile.GameArg) APIGameArg { return APIGameArg{ Flag: arg.Flag, DefaultValue: arg.DefaultValue, @@ -71,8 +71,8 @@ func toAPIGameArg(arg runfilemanager.GameArg) APIGameArg { } } -// toAPIRunFile converts runfilemanager.RunFile to APIRunFile -func toAPIRunFile(rf *runfilemanager.RunFile) APIRunFile { +// toAPIRunFile converts runfile.RunFile to APIRunFile +func toAPIRunFile(rf *runfile.RunFile) APIRunFile { apiArgs := make(map[string][]APIGameArg) for category, args := range rf.Args { for _, arg := range args { @@ -105,13 +105,13 @@ func writeJSONResponse(w http.ResponseWriter, status int, data interface{}, errM // HandleRunfileGroups handles GET /api/v2/runfile/groups func HandleRunfileGroups(w http.ResponseWriter, r *http.Request) { logger.Runfile.Debug("GET /api/v2/runfile/groups") - if runfilemanager.CurrentRunfile == nil { + if runfile.CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") return } - groups := runfilemanager.GetUIGroups() + groups := runfile.GetUIGroups() logger.Runfile.Info("fetched UI groups") writeJSONResponse(w, http.StatusOK, groups, "") } @@ -121,16 +121,16 @@ func HandleRunfileArgs(w http.ResponseWriter, r *http.Request) { group := r.URL.Query().Get("group") logger.Runfile.Debug(fmt.Sprintf("GET /api/v2/runfile/args group=%s", group)) - if runfilemanager.CurrentRunfile == nil { + if runfile.CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") return } - var args []runfilemanager.GameArg + var args []runfile.GameArg if group != "" { // Validate group - validGroups := runfilemanager.GetUIGroups() + validGroups := runfile.GetUIGroups() valid := false for _, g := range validGroups { if g == group { @@ -143,9 +143,9 @@ func HandleRunfileArgs(w http.ResponseWriter, r *http.Request) { writeJSONResponse(w, http.StatusBadRequest, nil, fmt.Sprintf("invalid group: %s", group)) return } - args = runfilemanager.GetArgsByGroup(group) + args = runfile.GetArgsByGroup(group) } else { - args = runfilemanager.GetAllArgs() + args = runfile.GetAllArgs() } // Convert to APIGameArg @@ -162,7 +162,7 @@ func HandleRunfileArgs(w http.ResponseWriter, r *http.Request) { func HandleRunfileArgUpdate(w http.ResponseWriter, r *http.Request) { logger.Runfile.Debug("POST /api/v2/runfile/args") - if runfilemanager.CurrentRunfile == nil { + if runfile.CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") return @@ -184,7 +184,7 @@ func HandleRunfileArgUpdate(w http.ResponseWriter, r *http.Request) { return } - if err := runfilemanager.SetArgValue(req.Flag, req.Value); err != nil { + if err := runfile.SetArgValue(req.Flag, req.Value); err != nil { logger.Runfile.Error(fmt.Sprintf("failed to set arg %s: %v", req.Flag, err)) writeJSONResponse(w, http.StatusBadRequest, nil, fmt.Sprintf("failed to set arg: %v", err)) return @@ -198,13 +198,13 @@ func HandleRunfileArgUpdate(w http.ResponseWriter, r *http.Request) { func HandleRunfile(w http.ResponseWriter, r *http.Request) { logger.Runfile.Debug("GET /api/v2/runfile") - if runfilemanager.CurrentRunfile == nil { + if runfile.CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") return } - apiRunfile := toAPIRunFile(runfilemanager.CurrentRunfile) + apiRunfile := toAPIRunFile(runfile.CurrentRunfile) logger.Runfile.Info("fetched runfile") writeJSONResponse(w, http.StatusOK, apiRunfile, "") } @@ -213,13 +213,13 @@ func HandleRunfile(w http.ResponseWriter, r *http.Request) { func HandleRunfileSave(w http.ResponseWriter, r *http.Request) { logger.Runfile.Debug("POST /api/v2/runfile/save") - if runfilemanager.CurrentRunfile == nil { + if runfile.CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") writeJSONResponse(w, http.StatusInternalServerError, nil, "runfile not loaded") return } - if err := runfilemanager.SaveRunfile(); err != nil { + if err := runfile.SaveRunfile(); err != nil { logger.Runfile.Error(fmt.Sprintf("failed to save runfile: %v", err)) writeJSONResponse(w, http.StatusInternalServerError, nil, fmt.Sprintf("failed to save runfile: %v", err)) return diff --git a/src/web/runfilegallery.go b/src/web/runfilegallery.go index 68fffccc..545349c3 100644 --- a/src/web/runfilegallery.go +++ b/src/web/runfilegallery.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfilegallery" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/gallery" ) // response wraps API responses @@ -21,7 +21,7 @@ func galleryHandler(w http.ResponseWriter, r *http.Request) { logger.Runfile.Info("Handling GET /api/v2/gallery request") forceUpdate := strings.ToLower(r.URL.Query().Get("forceUpdate")) == "true" - runfiles, err := runfilegallery.GetRunfileGallery(forceUpdate) + runfiles, err := gallery.GetRunfileGallery(forceUpdate) if err != nil { logger.Runfile.Error("Gallery fetch failed: " + err.Error()) sendResponse(w, http.StatusInternalServerError, response{Error: err.Error()}) @@ -51,7 +51,7 @@ func selectHandler(w http.ResponseWriter, r *http.Request) { return } - if err := runfilegallery.SaveRunfileToDisk(req.Identifier); err != nil { + if err := gallery.SaveRunfileToDisk(req.Identifier); err != nil { logger.Runfile.Error("Failed to save runfile " + req.Identifier + ": " + err.Error()) sendResponse(w, http.StatusInternalServerError, response{Error: err.Error()}) return From 085ca89cd088f02c3cbeb6685c85fbce2413644d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:05:09 +0200 Subject: [PATCH 07/93] added persistent IsSteamServerUI setting and switch in steamcmd accordingly --- src/config/config.go | 6 ++++++ src/config/getters.go | 6 ++++++ src/config/setters.go | 8 ++++++++ src/config/vars.go | 1 + src/setup/install.go | 4 ++-- src/steamcmd/steamcmd.go | 32 +++++++++++++++++++++----------- 6 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 26fcf72f..db502bc5 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -73,6 +73,7 @@ type JsonConfig struct { AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` SSUIIdentifier string `json:"SSUIIdentifier"` SSUIWebPort string `json:"SSUIWebPort"` + IsSteamServerUI *bool `json:"IsSteamServerUI"` // Update Settings IsUpdateEnabled *bool `json:"IsUpdateEnabled"` @@ -265,6 +266,10 @@ func applyConfig(cfg *JsonConfig) { AutoStartServerOnStartup = autoStartServerOnStartupVal cfg.AutoStartServerOnStartup = &autoStartServerOnStartupVal + isSteamServerUIVal := getBool(cfg.IsSteamServerUI, "IS_STEAM_SERVER_UI", false) + IsSteamServerUI = isSteamServerUIVal + cfg.IsSteamServerUI = &isSteamServerUIVal + // Process SaveInfo to maintain backwards compatibility with pre-5.6.6 SaveInfo field (deprecated) if SaveInfo != "" { parts := strings.Split(SaveInfo, " ") @@ -365,6 +370,7 @@ func safeSaveConfig() error { AutoStartServerOnStartup: &AutoStartServerOnStartup, SSUIIdentifier: SSUIIdentifier, SSUIWebPort: SSUIWebPort, + IsSteamServerUI: &IsSteamServerUI, } file, err := os.Create(ConfigPath) diff --git a/src/config/getters.go b/src/config/getters.go index 090fdff5..1abf1c6b 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -519,3 +519,9 @@ func GetRunfilesFolder() string { defer ConfigMu.Unlock() return RunFilesFolder } + +func GetIsSteamServerUI() bool { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return IsSteamServerUI +} diff --git a/src/config/setters.go b/src/config/setters.go index 58627abb..b1b3654e 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -86,6 +86,14 @@ func SetWorldID(value string) error { return nil } +func SetIsSteamServerUI(value bool) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + IsSteamServerUI = value + return safeSaveConfig() +} + // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT // ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT diff --git a/src/config/vars.go b/src/config/vars.go index 6db79528..d40daa58 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,6 +61,7 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string + IsSteamServerUI bool ) // Runtime only variables diff --git a/src/setup/install.go b/src/setup/install.go index 99e1d82d..c29ce7f4 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -42,8 +42,8 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("✅Blacklist.txt verified or created.") // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") - if config.GetSkipSteamCMD() { - logger.Install.Info("✅Skipping SteamCMD installation, SkipSteamCMD is true") + if config.GetSkipSteamCMD() || config.GetIsSteamServerUI() { + logger.Install.Info("✅Skipping SteamCMD installation, SkipSteamCMD is true or IsSteamServerUI is true") } else { steamcmd.InstallAndRunSteamCMD() } diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index 7312e714..cd7e2f7b 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -12,6 +12,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" ) @@ -61,7 +62,7 @@ func InstallAndRunSteamCMD() (int, error) { return installSteamCMDLinux() default: err := fmt.Errorf("SteamCMD installation is not supported on this OS") - logger.Install.Error("❌ " + err.Error() + "\n") + logger.Install.Error("❌ " + err.Error()) return -1, err } } @@ -81,15 +82,15 @@ func runSteamCMD(steamCMDDir string) (int, error) { defer logger.Core.Debug("🔄 Unlocking SteamMu after SteamCMD execution...") currentDir, err := os.Getwd() if err != nil { - logger.Install.Error("❌ Error getting current working directory: " + err.Error() + "\n") + logger.Install.Error("❌ Error getting current working directory: " + err.Error()) return -1, err } - logger.Install.Debug("✅ Current working directory: " + currentDir + "\n") + logger.Install.Debug("✅ Current working directory: " + currentDir) // Ensure permissions every time if we run on linux if runtime.GOOS != "windows" { if err := setExecutablePermissions(steamCMDDir); err != nil { - logger.Install.Error("❌ Error setting executable permissions, your Steamcmd install might be broken: " + err.Error() + "\n") + logger.Install.Error("❌ Error setting executable permissions, your Steamcmd install might be broken: " + err.Error()) return -1, err } } @@ -130,24 +131,33 @@ func runSteamCMD(steamCMDDir string) (int, error) { err = cmd.Run() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { - logger.Install.Error("❌ SteamCMD exited unsuccessfully: " + err.Error() + "\n") + logger.Install.Error("❌ SteamCMD exited unsuccessfully: " + err.Error()) return exitErr.ExitCode(), err } - logger.Install.Error("❌ Error running SteamCMD: " + err.Error() + "\n") + logger.Install.Error("❌ Error running SteamCMD: " + err.Error()) return -1, err } - logger.Install.Info("✅ SteamCMD executed successfully.\n") + logger.Install.Info("✅ SteamCMD executed successfully.") return 0, nil } // buildSteamCMDCommand constructs the SteamCMD command based on the OS. func buildSteamCMDCommand(steamCMDDir, currentDir string) *exec.Cmd { //print the config.GameBranch and config.GameServerAppID - logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) - logger.Install.Debug("🔍 Game Server App ID: " + config.GetGameServerAppID()) + steamAppID := config.GetGameServerAppID() + if config.GetIsSteamServerUI() { + logger.Install.Info("🔍 SSUI Runfile Identifier: " + runfile.CurrentRunfile.Meta.Name) + logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) + logger.Install.Info("🔍 Game Server App ID: " + runfile.CurrentRunfile.SteamAppID) + steamAppID = runfile.CurrentRunfile.SteamAppID + } else { + logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) + logger.Install.Info("🔍 Game Server App ID: " + config.GetGameServerAppID()) + + } if runtime.GOOS == "windows" { - return exec.Command(filepath.Join(steamCMDDir, "steamcmd.exe"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", config.GetGameServerAppID(), "-beta", config.GetGameBranch(), "validate", "+quit") + return exec.Command(filepath.Join(steamCMDDir, "steamcmd.exe"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", steamAppID, "-beta", config.GetGameBranch(), "validate", "+quit") } - return exec.Command(filepath.Join(steamCMDDir, "steamcmd.sh"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", config.GetGameServerAppID(), "-beta", config.GetGameBranch(), "validate", "+quit") + return exec.Command(filepath.Join(steamCMDDir, "steamcmd.sh"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", steamAppID, "-beta", config.GetGameBranch(), "validate", "+quit") } From 9564c4c25c98c8f0498bbc01f1a6cb150ac806f9 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:17:32 +0200 Subject: [PATCH 08/93] refactored runfile handling to use persistent "RunfileIdentifier"; updated getters/setters and removed deprecated variable --- src/config/config.go | 49 +++++++++++++++-------------- src/config/getters.go | 6 ---- src/config/steamserverui-getters.go | 11 +++++-- src/config/steamserverui-setter.go | 4 +-- src/config/steamserverui-vars.go | 5 --- src/config/vars.go | 7 ++++- src/core/loader/runfile.go | 4 +-- src/steamserverui/runfile/args.go | 2 +- 8 files changed, 46 insertions(+), 42 deletions(-) delete mode 100644 src/config/steamserverui-vars.go diff --git a/src/config/config.go b/src/config/config.go index db502bc5..451c14d1 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -26,29 +26,30 @@ type JsonConfig struct { // reordered in 5.6.4 to simplify the order of the config file. // Gameserver Settings - GameBranch string `json:"gameBranch"` - GamePort string `json:"GamePort"` - ServerName string `json:"ServerName"` - SaveInfo string `json:"SaveInfo,omitempty"` // deprecated, kept for backwards compatibility - SaveName string `json:"SaveName"` // replaces SaveInfo - WorldID string `json:"WorldID"` // replaces SaveInfo - ServerMaxPlayers string `json:"ServerMaxPlayers"` - ServerPassword string `json:"ServerPassword"` - ServerAuthSecret string `json:"ServerAuthSecret"` - AdminPassword string `json:"AdminPassword"` - UpdatePort string `json:"UpdatePort"` - UPNPEnabled *bool `json:"UPNPEnabled"` - AutoSave *bool `json:"AutoSave"` - SaveInterval string `json:"SaveInterval"` - AutoPauseServer *bool `json:"AutoPauseServer"` - LocalIpAddress string `json:"LocalIpAddress"` - StartLocalHost *bool `json:"StartLocalHost"` - ServerVisible *bool `json:"ServerVisible"` - UseSteamP2P *bool `json:"UseSteamP2P"` - AdditionalParams string `json:"AdditionalParams"` - Difficulty string `json:"Difficulty"` - StartCondition string `json:"StartCondition"` - StartLocation string `json:"StartLocation"` + RunfileIdentifier string `json:"RunfileIdentifier"` + GameBranch string `json:"gameBranch"` + GamePort string `json:"GamePort"` + ServerName string `json:"ServerName"` + SaveInfo string `json:"SaveInfo,omitempty"` // deprecated, kept for backwards compatibility + SaveName string `json:"SaveName"` // replaces SaveInfo + WorldID string `json:"WorldID"` // replaces SaveInfo + ServerMaxPlayers string `json:"ServerMaxPlayers"` + ServerPassword string `json:"ServerPassword"` + ServerAuthSecret string `json:"ServerAuthSecret"` + AdminPassword string `json:"AdminPassword"` + UpdatePort string `json:"UpdatePort"` + UPNPEnabled *bool `json:"UPNPEnabled"` + AutoSave *bool `json:"AutoSave"` + SaveInterval string `json:"SaveInterval"` + AutoPauseServer *bool `json:"AutoPauseServer"` + LocalIpAddress string `json:"LocalIpAddress"` + StartLocalHost *bool `json:"StartLocalHost"` + ServerVisible *bool `json:"ServerVisible"` + UseSteamP2P *bool `json:"UseSteamP2P"` + AdditionalParams string `json:"AdditionalParams"` + Difficulty string `json:"Difficulty"` + StartCondition string `json:"StartCondition"` + StartLocation string `json:"StartLocation"` // Logging and debug settings Debug *bool `json:"Debug"` @@ -143,6 +144,7 @@ func applyConfig(cfg *JsonConfig) { ControlPanelChannelID = getString(cfg.ControlPanelChannelID, "CONTROL_PANEL_CHANNEL_ID", "") DiscordCharBufferSize = getInt(cfg.DiscordCharBufferSize, "DISCORD_CHAR_BUFFER_SIZE", 1000) BlackListFilePath = getString(cfg.BlackListFilePath, "BLACKLIST_FILE_PATH", "./Blacklist.txt") + RunfileIdentifier = getString(cfg.RunfileIdentifier, "RUNFILE_IDENTIFIER", "") isDiscordEnabledVal := getBool(cfg.IsDiscordEnabled, "IS_DISCORD_ENABLED", false) IsDiscordEnabled = isDiscordEnabledVal @@ -371,6 +373,7 @@ func safeSaveConfig() error { SSUIIdentifier: SSUIIdentifier, SSUIWebPort: SSUIWebPort, IsSteamServerUI: &IsSteamServerUI, + RunfileIdentifier: RunfileIdentifier, } file, err := os.Create(ConfigPath) diff --git a/src/config/getters.go b/src/config/getters.go index 1abf1c6b..090fdff5 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -519,9 +519,3 @@ func GetRunfilesFolder() string { defer ConfigMu.Unlock() return RunFilesFolder } - -func GetIsSteamServerUI() bool { - ConfigMu.Lock() - defer ConfigMu.Unlock() - return IsSteamServerUI -} diff --git a/src/config/steamserverui-getters.go b/src/config/steamserverui-getters.go index 7b4138c3..a9806fc8 100644 --- a/src/config/steamserverui-getters.go +++ b/src/config/steamserverui-getters.go @@ -1,5 +1,12 @@ package config +// GetIsSteamServerUI returns if the system is in SteamServerUI mode +func GetIsSteamServerUI() bool { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return IsSteamServerUI +} + // GetRunFilesFolder returns the RunFilesFolder func GetRunFilesFolder() string { ConfigMu.Lock() @@ -8,8 +15,8 @@ func GetRunFilesFolder() string { } // GetRunfileGame returns the RunfileGame -func GetRunfileGame() string { +func GetRunfileIdentifier() string { ConfigMu.Lock() defer ConfigMu.Unlock() - return RunfileGame + return RunfileIdentifier } diff --git a/src/config/steamserverui-setter.go b/src/config/steamserverui-setter.go index d79fde4e..a2958081 100644 --- a/src/config/steamserverui-setter.go +++ b/src/config/steamserverui-setter.go @@ -6,7 +6,7 @@ import ( ) // SetRunfileGame sets the RunfileGame with validation -func SetRunfileGame(value string) error { +func SetRunfileIdentifier(value string) error { ConfigMu.Lock() defer ConfigMu.Unlock() @@ -14,7 +14,7 @@ func SetRunfileGame(value string) error { return fmt.Errorf("runfile game cannot be empty") } - RunfileGame = value + RunfileIdentifier = value return nil //return saveConfig() } diff --git a/src/config/steamserverui-vars.go b/src/config/steamserverui-vars.go deleted file mode 100644 index e817e5a2..00000000 --- a/src/config/steamserverui-vars.go +++ /dev/null @@ -1,5 +0,0 @@ -package config - -var ( - RunfileGame string -) diff --git a/src/config/vars.go b/src/config/vars.go index d40daa58..6e7f8359 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -61,7 +61,12 @@ var ( LanguageSetting string AutoStartServerOnStartup bool SSUIIdentifier string - IsSteamServerUI bool +) + +// SteamServerUI Settings +var ( + IsSteamServerUI bool + RunfileIdentifier string ) // Runtime only variables diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 1393c764..95a96baf 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -21,7 +21,7 @@ func InitRunfile(game string) error { logger.Runfile.Info("Updating runfile game to " + game) logger.Runfile.Info("Stopping server if running") gamemgr.InternalStopServer() - config.SetRunfileGame(game) + config.SetRunfileIdentifier(game) if err := ReloadRunfile(); err != nil { return err @@ -37,7 +37,7 @@ func InitRunfile(game string) error { // used to only reload runfile into memory. Can be triggered from v1 UI -> Runfile Reset terminal func ReloadRunfile() error { - if err := runfile.LoadRunfile(config.GetRunfileGame(), config.GetRunFilesFolder()); err != nil { + if err := runfile.LoadRunfile(config.GetRunfileIdentifier(), config.GetRunFilesFolder()); err != nil { logger.Runfile.Warn("Failed to reload runfile: " + err.Error()) return err } diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index 44d8604a..3faf86a1 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -228,7 +228,7 @@ func SaveRunfile() error { } // Build filepath - filePath := filepath.Join(config.GetRunFilesFolder(), fmt.Sprintf("run%s.ssui", config.GetRunfileGame())) + filePath := filepath.Join(config.GetRunFilesFolder(), fmt.Sprintf("run%s.ssui", config.GetRunfileIdentifier())) logger.Runfile.Debug(fmt.Sprintf("saving runfile: path=%s", filePath)) // Update DefaultValue from RuntimeValue From de6d5737506d7217110141519bebdfdd509dd116 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:55:47 +0200 Subject: [PATCH 09/93] added runfile loading to loader --- src/core/loader/loader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 6c224cc4..c0e7631a 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -23,6 +23,7 @@ func InitBackend(wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() ReloadConfig() + ReloadRunfile() ReloadSSCM() ReloadBackupManager() ReloadLocalizer() From 09d45af9c311301fb425f95a8088bfe05e2d018a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:56:15 +0200 Subject: [PATCH 10/93] added steamcmd call to loader when init runfile is called --- src/core/loader/runfile.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 95a96baf..927a976b 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -7,6 +7,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) @@ -27,16 +28,18 @@ func InitRunfile(game string) error { return err } - logger.Runfile.Info("Running SteamCMD, this may take a while...") - //steammgr.RunSteamCMD() - logger.Runfile.Warn("Steamcmd for runfile not implemented yet") logger.Runfile.Info("Runfile game updated to " + game) + logger.Runfile.Info("Running SteamCMD, this may take a while...") + steamcmd.InstallAndRunSteamCMD() return nil } -// used to only reload runfile into memory. Can be triggered from v1 UI -> Runfile Reset terminal func ReloadRunfile() error { + if !config.GetIsSteamServerUI() { + return nil + } + if err := runfile.LoadRunfile(config.GetRunfileIdentifier(), config.GetRunFilesFolder()); err != nil { logger.Runfile.Warn("Failed to reload runfile: " + err.Error()) return err From ffc9d85d46a2c9fe88e9c44f772418374781f95d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:08:12 +0200 Subject: [PATCH 11/93] small logging and testing improvements --- src/core/loader/runfile.go | 1 + src/steamcmd/steamcmd.go | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 927a976b..4abe145d 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -37,6 +37,7 @@ func InitRunfile(game string) error { func ReloadRunfile() error { if !config.GetIsSteamServerUI() { + logger.Runfile.Warn("Runfile reloading is only supported in SteamServerUI mode") return nil } diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index cd7e2f7b..7c139ac1 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -35,6 +35,10 @@ const ( // InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux). // It returns the exit status of the SteamCMD execution and any error encountered. func InstallAndRunSteamCMD() (int, error) { + if config.GetSkipSteamCMD() { + logger.Install.Info("✅Skipping SteamCMD installation") + } + if isUpdatingMu.TryLock() { // Successfully acquired the lock; we are not updating currently logger.Core.Debug("🔄 Locking isUpdatingMu for SteamCMD Update run...") From 0b48572694f95c51f4c795ece7232747d6f1f01c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:13:01 +0200 Subject: [PATCH 12/93] renamed "default" value in runfiles to "value" as that field stores the set value, not only the default --- src/steamserverui/runfile/args.go | 10 +++++----- src/web/runfile.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index 3faf86a1..6f86b5bb 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -52,7 +52,7 @@ func (e ErrValidation) Error() string { type GameArg struct { Flag string `json:"flag"` - DefaultValue string `json:"default"` + Value string `json:"value"` RuntimeValue string `json:"-"` Required bool `json:"required"` RequiresValue bool `json:"requires_value"` @@ -196,10 +196,10 @@ func LoadRunfile(gameName, runFilesFolder string) error { // Initialize runtime values *before* validation for category := range runfile.Args { for i := range runfile.Args[category] { - runfile.Args[category][i].RuntimeValue = runfile.Args[category][i].DefaultValue + runfile.Args[category][i].RuntimeValue = runfile.Args[category][i].Value logger.Runfile.Debug(fmt.Sprintf("initialized arg: flag=%s, default=%s, runtime=%s", runfile.Args[category][i].Flag, - runfile.Args[category][i].DefaultValue, + runfile.Args[category][i].Value, runfile.Args[category][i].RuntimeValue)) } } @@ -231,10 +231,10 @@ func SaveRunfile() error { filePath := filepath.Join(config.GetRunFilesFolder(), fmt.Sprintf("run%s.ssui", config.GetRunfileIdentifier())) logger.Runfile.Debug(fmt.Sprintf("saving runfile: path=%s", filePath)) - // Update DefaultValue from RuntimeValue + // Update Value from RuntimeValue for category := range CurrentRunfile.Args { for i := range CurrentRunfile.Args[category] { - CurrentRunfile.Args[category][i].DefaultValue = CurrentRunfile.Args[category][i].RuntimeValue + CurrentRunfile.Args[category][i].Value = CurrentRunfile.Args[category][i].RuntimeValue } } diff --git a/src/web/runfile.go b/src/web/runfile.go index 87a1f22a..27ff62cf 100644 --- a/src/web/runfile.go +++ b/src/web/runfile.go @@ -14,7 +14,7 @@ import ( // APIGameArg is a DTO for GameArg, including RuntimeValue and all fields type APIGameArg struct { Flag string `json:"flag"` - DefaultValue string `json:"default"` + Value string `json:"value"` RuntimeValue string `json:"runtime_value"` Required bool `json:"required"` RequiresValue bool `json:"requires_value"` @@ -55,7 +55,7 @@ type apiResponse struct { func toAPIGameArg(arg runfile.GameArg) APIGameArg { return APIGameArg{ Flag: arg.Flag, - DefaultValue: arg.DefaultValue, + Value: arg.Value, RuntimeValue: arg.RuntimeValue, Required: arg.Required, RequiresValue: arg.RequiresValue, From 8e224ea31c0c56f8d9ff18f4e67a06443d907149 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 17:17:01 +0200 Subject: [PATCH 13/93] renamed setting "IsSteamServerUI" to "UseRunfiles" --- src/config/config.go | 10 +++++----- src/config/setters.go | 4 ++-- src/config/steamserverui-getters.go | 4 ++-- src/config/vars.go | 2 +- src/core/loader/runfile.go | 2 +- src/setup/install.go | 2 +- src/steamcmd/steamcmd.go | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 451c14d1..3dae6c5b 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -74,7 +74,7 @@ type JsonConfig struct { AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` SSUIIdentifier string `json:"SSUIIdentifier"` SSUIWebPort string `json:"SSUIWebPort"` - IsSteamServerUI *bool `json:"IsSteamServerUI"` + UseRunfiles *bool `json:"UseRunfiles"` // Update Settings IsUpdateEnabled *bool `json:"IsUpdateEnabled"` @@ -268,9 +268,9 @@ func applyConfig(cfg *JsonConfig) { AutoStartServerOnStartup = autoStartServerOnStartupVal cfg.AutoStartServerOnStartup = &autoStartServerOnStartupVal - isSteamServerUIVal := getBool(cfg.IsSteamServerUI, "IS_STEAM_SERVER_UI", false) - IsSteamServerUI = isSteamServerUIVal - cfg.IsSteamServerUI = &isSteamServerUIVal + isUseRunfilesVal := getBool(cfg.UseRunfiles, "USE_RUNFILES", true) + UseRunfiles = isUseRunfilesVal + cfg.UseRunfiles = &isUseRunfilesVal // Process SaveInfo to maintain backwards compatibility with pre-5.6.6 SaveInfo field (deprecated) if SaveInfo != "" { @@ -372,7 +372,7 @@ func safeSaveConfig() error { AutoStartServerOnStartup: &AutoStartServerOnStartup, SSUIIdentifier: SSUIIdentifier, SSUIWebPort: SSUIWebPort, - IsSteamServerUI: &IsSteamServerUI, + UseRunfiles: &UseRunfiles, RunfileIdentifier: RunfileIdentifier, } diff --git a/src/config/setters.go b/src/config/setters.go index b1b3654e..1b1acbf3 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -86,11 +86,11 @@ func SetWorldID(value string) error { return nil } -func SetIsSteamServerUI(value bool) error { +func SetUseRunfiles(value bool) error { ConfigMu.Lock() defer ConfigMu.Unlock() - IsSteamServerUI = value + UseRunfiles = value return safeSaveConfig() } diff --git a/src/config/steamserverui-getters.go b/src/config/steamserverui-getters.go index a9806fc8..b04d9d9f 100644 --- a/src/config/steamserverui-getters.go +++ b/src/config/steamserverui-getters.go @@ -1,10 +1,10 @@ package config // GetIsSteamServerUI returns if the system is in SteamServerUI mode -func GetIsSteamServerUI() bool { +func GetUseRunfiles() bool { ConfigMu.Lock() defer ConfigMu.Unlock() - return IsSteamServerUI + return UseRunfiles } // GetRunFilesFolder returns the RunFilesFolder diff --git a/src/config/vars.go b/src/config/vars.go index 6e7f8359..3873999d 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -65,7 +65,7 @@ var ( // SteamServerUI Settings var ( - IsSteamServerUI bool + UseRunfiles bool RunfileIdentifier string ) diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 4abe145d..7357bf05 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -36,7 +36,7 @@ func InitRunfile(game string) error { } func ReloadRunfile() error { - if !config.GetIsSteamServerUI() { + if !config.GetUseRunfiles() { logger.Runfile.Warn("Runfile reloading is only supported in SteamServerUI mode") return nil } diff --git a/src/setup/install.go b/src/setup/install.go index c29ce7f4..2dcb07ee 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -42,7 +42,7 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("✅Blacklist.txt verified or created.") // Step 3: Install and run SteamCMD logger.Install.Info("🔄Installing and running SteamCMD...") - if config.GetSkipSteamCMD() || config.GetIsSteamServerUI() { + if config.GetSkipSteamCMD() || config.GetUseRunfiles() { logger.Install.Info("✅Skipping SteamCMD installation, SkipSteamCMD is true or IsSteamServerUI is true") } else { steamcmd.InstallAndRunSteamCMD() diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index 7c139ac1..880a046e 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -149,7 +149,7 @@ func runSteamCMD(steamCMDDir string) (int, error) { func buildSteamCMDCommand(steamCMDDir, currentDir string) *exec.Cmd { //print the config.GameBranch and config.GameServerAppID steamAppID := config.GetGameServerAppID() - if config.GetIsSteamServerUI() { + if config.GetUseRunfiles() { logger.Install.Info("🔍 SSUI Runfile Identifier: " + runfile.CurrentRunfile.Meta.Name) logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) logger.Install.Info("🔍 Game Server App ID: " + runfile.CurrentRunfile.SteamAppID) From db8d7290cd8bc4b21924ac2557b54d7270cd4f60 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:07:28 +0200 Subject: [PATCH 14/93] skip steamcmd when runfiles are true for better testing --- src/steamcmd/steamcmd.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index 880a046e..124212c7 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -35,8 +35,9 @@ const ( // InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux). // It returns the exit status of the SteamCMD execution and any error encountered. func InstallAndRunSteamCMD() (int, error) { - if config.GetSkipSteamCMD() { + if config.GetSkipSteamCMD() || config.GetUseRunfiles() { logger.Install.Info("✅Skipping SteamCMD installation") + return -1, nil } if isUpdatingMu.TryLock() { From c9dc0f0801e31e3398da10087334406d520ede50 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 18:07:48 +0200 Subject: [PATCH 15/93] fix loggin in runfile init --- src/steamserverui/runfile/args.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index 6f86b5bb..204aa4a4 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -197,7 +197,7 @@ func LoadRunfile(gameName, runFilesFolder string) error { for category := range runfile.Args { for i := range runfile.Args[category] { runfile.Args[category][i].RuntimeValue = runfile.Args[category][i].Value - logger.Runfile.Debug(fmt.Sprintf("initialized arg: flag=%s, default=%s, runtime=%s", + logger.Runfile.Debug(fmt.Sprintf("initialized arg: flag=%s, value=%s, runtime=%s", runfile.Args[category][i].Flag, runfile.Args[category][i].Value, runfile.Args[category][i].RuntimeValue)) From 98fedc225f862f64096c849ea12faf78e46d2ac7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:03:21 +0200 Subject: [PATCH 16/93] add TestArgBuilder and register new CLI command for argument building functionality --- src/cli/runtimecommands.go | 6 ++++++ src/steamserverui/runfile/argexample.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 1e282b82..673a76e0 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -24,6 +24,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) // ANSI escape codes for green text and reset @@ -163,6 +164,7 @@ func init() { RegisterCommand("getbuildid", WrapNoReturn(getBuildID), "gbid") RegisterCommand("setdummybuildid", WrapNoReturn(setDummyBuildID), "sdbid") RegisterCommand("printconfig", WrapNoReturn(printConfig), "pc") + RegisterCommand("testargbuilder", WrapNoReturn(TestArgBuilder), "targb") } func startServer() { @@ -311,3 +313,7 @@ func supportPackage() { w, _ = zw.Create("system_info.txt") w.Write([]byte(info)) } + +func TestArgBuilder() { + runfile.TestArgBuilder() +} diff --git a/src/steamserverui/runfile/argexample.go b/src/steamserverui/runfile/argexample.go index 23d43d57..ad248216 100644 --- a/src/steamserverui/runfile/argexample.go +++ b/src/steamserverui/runfile/argexample.go @@ -49,3 +49,11 @@ func Examples() { } fmt.Println(arg.RuntimeValue) // Access the argument's properties } + +func TestArgBuilder() { + args, err := BuildCommandArgs() + if err != nil { + panic(err) + } + fmt.Println(args) +} From a471203a534011e4f4a16347b183752e7e02ca8d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 3 Oct 2025 19:04:16 +0200 Subject: [PATCH 17/93] changed BuildCommandArgs to allow appending the value but not the flag (for use with -file start arguments) --- src/steamserverui/runfile/args.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index 204aa4a4..1b77445c 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -347,15 +347,11 @@ func BuildCommandArgs() ([]string, error) { }) for _, arg := range allArgs { - if arg.Disabled { - continue - } - if !arg.Required && arg.RequiresValue && arg.RuntimeValue == "" { + if arg.Disabled || (!arg.Required && arg.RequiresValue && arg.RuntimeValue == "") { //Clear text for clarity: skip if disabled OR if it's an optional argument that needs a value but doesn't have one set continue } - args = append(args, arg.Flag) - // Special handling + // Handle space_delimited: split and append non-empty parts if arg.Special == "space_delimited" { parts := strings.Split(arg.RuntimeValue, " ") for _, part := range parts { @@ -366,7 +362,12 @@ func BuildCommandArgs() ([]string, error) { continue } - // Only add value if the argument requires one + // Append non-empty flags + if arg.Flag != "" && arg.Special != "dont_append_flag_just_value" { + args = append(args, arg.Flag) + } + + // Append value if required and non-empty if arg.RequiresValue && arg.RuntimeValue != "" { args = append(args, arg.RuntimeValue) } From 229fbd16743283c33c7aace28f1f075f03f92500 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 4 Oct 2025 15:07:46 +0200 Subject: [PATCH 18/93] save config when RunfileIdentifier is updated --- src/config/steamserverui-setter.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/config/steamserverui-setter.go b/src/config/steamserverui-setter.go index a2958081..7a0cf22d 100644 --- a/src/config/steamserverui-setter.go +++ b/src/config/steamserverui-setter.go @@ -15,6 +15,5 @@ func SetRunfileIdentifier(value string) error { } RunfileIdentifier = value - return nil - //return saveConfig() + return safeSaveConfig() } From a4f227c25e0503f7db6d6a0d6f03862f65882b2c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 4 Oct 2025 21:27:47 +0200 Subject: [PATCH 19/93] added a arg.Special to hide args from the rf on the UI --- src/steamserverui/runfile/getters.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/steamserverui/runfile/getters.go b/src/steamserverui/runfile/getters.go index c56075fc..b6770787 100644 --- a/src/steamserverui/runfile/getters.go +++ b/src/steamserverui/runfile/getters.go @@ -35,6 +35,7 @@ func GetUIGroups() []string { return result } +// GetArgsByGroup returns GameArgs from the rf for use in UI context - and skips args that have arg.Special is not "hide_in_ui" func GetArgsByGroup(group string) []GameArg { if CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") @@ -44,7 +45,9 @@ func GetArgsByGroup(group string) []GameArg { var result []GameArg for _, arg := range CurrentRunfile.getAllArgs() { if arg.UIGroup == group { - result = append(result, arg) + if arg.Special != "hide_in_ui" { + result = append(result, arg) + } } } return result From a36f8030d8f5b8ee229ef63d0ecf833f95fb0504 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:55:42 +0200 Subject: [PATCH 20/93] use runfile for steamcmd auto update poller if configured, otherwise fallback to Stationeers game server app ID --- src/steamcmd/getappinfo.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index 53e68541..57be19fe 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -17,6 +17,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) var ( @@ -84,8 +85,13 @@ func getAppInfo() error { } steamcmddir := SteamCMDLinuxDir executable := "steamcmd.sh" + appid := config.GetGameServerAppID() + if config.GetUseRunfiles() { + appid = runfile.CurrentRunfile.SteamAppID + } + if runtime.GOOS == "windows" { executable = "steamcmd.exe" steamcmddir = SteamCMDWindowsDir From e118d278c9a86609f0599b57c7709bcae40afeda Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:56:05 +0200 Subject: [PATCH 21/93] added IsStationeersMode setting --- src/config/config.go | 5 +++++ src/config/getters.go | 6 ++++++ src/config/vars.go | 1 + src/core/loader/loader.go | 26 +++++++++++++++----------- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 3dae6c5b..5c4336a2 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -75,6 +75,7 @@ type JsonConfig struct { SSUIIdentifier string `json:"SSUIIdentifier"` SSUIWebPort string `json:"SSUIWebPort"` UseRunfiles *bool `json:"UseRunfiles"` + IsStationeersMode *bool `json:"StationeersMode"` // Update Settings IsUpdateEnabled *bool `json:"IsUpdateEnabled"` @@ -272,6 +273,10 @@ func applyConfig(cfg *JsonConfig) { UseRunfiles = isUseRunfilesVal cfg.UseRunfiles = &isUseRunfilesVal + isStationeersModeVal := getBool(cfg.IsStationeersMode, "STATIONEERS_MODE", false) + IsStationeersMode = isStationeersModeVal + cfg.IsStationeersMode = &isStationeersModeVal + // Process SaveInfo to maintain backwards compatibility with pre-5.6.6 SaveInfo field (deprecated) if SaveInfo != "" { parts := strings.Split(SaveInfo, " ") diff --git a/src/config/getters.go b/src/config/getters.go index 090fdff5..8f2cce2e 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -519,3 +519,9 @@ func GetRunfilesFolder() string { defer ConfigMu.Unlock() return RunFilesFolder } + +func GetIsStationeersMode() bool { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return IsStationeersMode +} diff --git a/src/config/vars.go b/src/config/vars.go index 3873999d..6b9d8f0d 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -67,6 +67,7 @@ var ( var ( UseRunfiles bool RunfileIdentifier string + IsStationeersMode bool ) // Runtime only variables diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index c0e7631a..bf6a2b77 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -24,8 +24,8 @@ func InitBackend(wg *sync.WaitGroup) { defer wg.Done() ReloadConfig() ReloadRunfile() - ReloadSSCM() - ReloadBackupManager() + ReloadStationeersSSCM() + ReloadStationeersBackupManager() ReloadLocalizer() ReloadAppInfoPoller() ReloadDiscordBot() @@ -37,8 +37,8 @@ func ReloadBackend() { logger.Core.Info("Reloading backend...") ReloadConfig() - ReloadSSCM() - ReloadBackupManager() + ReloadStationeersSSCM() + ReloadStationeersBackupManager() ReloadLocalizer() ReloadAppInfoPoller() PrintConfigDetails() @@ -55,16 +55,20 @@ func ReloadConfig() { } -func ReloadSSCM() { - if config.GetIsSSCMEnabled() { - setup.InstallSSCM() +func ReloadStationeersSSCM() { + if config.GetIsStationeersMode() { + if config.GetIsSSCMEnabled() { + setup.InstallSSCM() + } } } -func ReloadBackupManager() { - if err := backupmgr.ReloadBackupManagerFromConfig(); err != nil { - logger.Backup.Error("Failed to reload backup manager: " + err.Error()) - return +func ReloadStationeersBackupManager() { + if config.GetIsStationeersMode() { + if err := backupmgr.ReloadBackupManagerFromConfig(); err != nil { + logger.Backup.Error("Failed to reload backup manager: " + err.Error()) + return + } } } From a1f1100f876aa882a7550e36ffa1b41cc84ea003 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 13:58:43 +0200 Subject: [PATCH 22/93] update Discord RPC activity wording --- src/discordrpc/discordrpc.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/discordrpc/discordrpc.go b/src/discordrpc/discordrpc.go index 1f4d0d49..e8bb22ec 100644 --- a/src/discordrpc/discordrpc.go +++ b/src/discordrpc/discordrpc.go @@ -12,10 +12,10 @@ import ( func StartDiscordRPC() (*discordrichpresence.Client, error) { client := discordrichpresence.NewClient("1408848834875887669") activity := discordrichpresence.NewActivity(). - State("Managing a Stationeers Server"). - Details("Your one-stop-shop for running a Stationeers server"). + State("Managing a Gameserver"). + Details("Your one-stop-shop for running a Gameserver"). StartTime(time.Now()). - LargeImage("logo", "The easy to use Stationeers Dedicated Server Manager"). + LargeImage("logo", "The easy to use Dedicated Server Manager"). SmallImage("rocket", "Online and Active"). Type(0). Build() From eb5bd1b6e2e605befbfed8366d2b124bc68fe3f4 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:04:25 +0200 Subject: [PATCH 23/93] unified setter & getter files --- src/config/getters.go | 21 +++++++++++++++++++++ src/config/setters.go | 17 ++++++++++++----- src/config/steamserverui-getters.go | 22 ---------------------- src/config/steamserverui-setter.go | 19 ------------------- 4 files changed, 33 insertions(+), 46 deletions(-) delete mode 100644 src/config/steamserverui-getters.go delete mode 100644 src/config/steamserverui-setter.go diff --git a/src/config/getters.go b/src/config/getters.go index 8f2cce2e..31ea5ca8 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -525,3 +525,24 @@ func GetIsStationeersMode() bool { defer ConfigMu.Unlock() return IsStationeersMode } + +// GetIsSteamServerUI returns if the system is in SteamServerUI mode +func GetUseRunfiles() bool { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return UseRunfiles +} + +// GetRunFilesFolder returns the RunFilesFolder +func GetRunFilesFolder() string { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return RunFilesFolder +} + +// GetRunfileGame returns the RunfileGame +func GetRunfileIdentifier() string { + ConfigMu.Lock() + defer ConfigMu.Unlock() + return RunfileIdentifier +} diff --git a/src/config/setters.go b/src/config/setters.go index 1b1acbf3..0f886bd2 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -94,11 +94,18 @@ func SetUseRunfiles(value bool) error { return safeSaveConfig() } -// ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT -// ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT -// ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT -// ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT -// ALL SETTERS BELOW THIS LINE ARE UNUSED AT THE MOMENT +// SetRunfileGame sets the RunfileGame with validation +func SetRunfileIdentifier(value string) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + if strings.TrimSpace(value) == "" { + return fmt.Errorf("runfile game cannot be empty") + } + + RunfileIdentifier = value + return safeSaveConfig() +} // Debug and Logging Settings func SetIsDebugMode(value bool) error { diff --git a/src/config/steamserverui-getters.go b/src/config/steamserverui-getters.go deleted file mode 100644 index b04d9d9f..00000000 --- a/src/config/steamserverui-getters.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -// GetIsSteamServerUI returns if the system is in SteamServerUI mode -func GetUseRunfiles() bool { - ConfigMu.Lock() - defer ConfigMu.Unlock() - return UseRunfiles -} - -// GetRunFilesFolder returns the RunFilesFolder -func GetRunFilesFolder() string { - ConfigMu.Lock() - defer ConfigMu.Unlock() - return RunFilesFolder -} - -// GetRunfileGame returns the RunfileGame -func GetRunfileIdentifier() string { - ConfigMu.Lock() - defer ConfigMu.Unlock() - return RunfileIdentifier -} diff --git a/src/config/steamserverui-setter.go b/src/config/steamserverui-setter.go deleted file mode 100644 index 7a0cf22d..00000000 --- a/src/config/steamserverui-setter.go +++ /dev/null @@ -1,19 +0,0 @@ -package config - -import ( - "fmt" - "strings" -) - -// SetRunfileGame sets the RunfileGame with validation -func SetRunfileIdentifier(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - if strings.TrimSpace(value) == "" { - return fmt.Errorf("runfile game cannot be empty") - } - - RunfileIdentifier = value - return safeSaveConfig() -} From b270df5230788e2858a2b16d05e2f9a3962b2bbe Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:32:24 +0200 Subject: [PATCH 24/93] re-enable steamcmd for testing --- src/steamcmd/steamcmd.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index 124212c7..d72fcc6e 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -35,10 +35,6 @@ const ( // InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux). // It returns the exit status of the SteamCMD execution and any error encountered. func InstallAndRunSteamCMD() (int, error) { - if config.GetSkipSteamCMD() || config.GetUseRunfiles() { - logger.Install.Info("✅Skipping SteamCMD installation") - return -1, nil - } if isUpdatingMu.TryLock() { // Successfully acquired the lock; we are not updating currently From 75390463070bb03e9849f1965dfbad8df0127f9a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:32:52 +0200 Subject: [PATCH 25/93] added v6 arg builder to startup --- src/managers/gamemgr/processmanagement.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/managers/gamemgr/processmanagement.go b/src/managers/gamemgr/processmanagement.go index 06e1a296..1f04fe71 100644 --- a/src/managers/gamemgr/processmanagement.go +++ b/src/managers/gamemgr/processmanagement.go @@ -14,6 +14,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) var ( @@ -35,6 +36,10 @@ func InternalStartServer() error { args := buildCommandArgs() + if config.UseRunfiles { + args, err = runfile.BuildCommandArgs() + } + logger.Core.Info("=== GAMESERVER STARTING ===") if config.GetIsSSCMEnabled() && runtime.GOOS == "linux" { From 5398c33e2b6f2bfabf2a88f9a6143a7d43628ab2 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 19:32:06 +0200 Subject: [PATCH 26/93] [Megacommit] Rework of many systems, see following lines: Add support for generic BepInEx without SSCM change BepInEx setup logic refactor SSCM integration across relevant files add better arg printing in startup for debugging switched to only supporting runfiles in v6 removed running steamcmd during startup entirely hooked up runfile arg builder fully this should have been multiple commits, whoops --- src/config/config.go | 9 +- src/config/getters.go | 13 ++- src/config/setters.go | 8 ++ src/config/vars.go | 5 +- src/core/loader/loader.go | 11 +- src/core/loader/runfile.go | 4 - src/managers/gamemgr/args.go | 118 ---------------------- src/managers/gamemgr/bepinex.go | 9 +- src/managers/gamemgr/processmanagement.go | 50 ++++++--- src/setup/{sscm.go => bepinex.go} | 12 --- src/setup/install.go | 8 -- src/steamcmd/getappinfo.go | 6 +- src/steamcmd/steamcmd.go | 15 +-- 13 files changed, 79 insertions(+), 189 deletions(-) delete mode 100644 src/managers/gamemgr/args.go rename src/setup/{sscm.go => bepinex.go} (95%) diff --git a/src/config/config.go b/src/config/config.go index 5c4336a2..aaba18e9 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -68,6 +68,7 @@ type JsonConfig struct { ExePath string `json:"ExePath"` LogClutterToConsole *bool `json:"LogClutterToConsole"` IsSSCMEnabled *bool `json:"IsSSCMEnabled"` + IsBepInExEnabled *bool `json:"IsBepInExEnabled"` AutoRestartServerTimer string `json:"AutoRestartServerTimer"` IsConsoleEnabled *bool `json:"IsConsoleEnabled"` LanguageSetting string `json:"LanguageSetting"` @@ -253,7 +254,12 @@ func applyConfig(cfg *JsonConfig) { SubsystemFilters = getStringSlice(cfg.SubsystemFilters, "SUBSYSTEM_FILTERS", []string{}) AutoRestartServerTimer = getString(cfg.AutoRestartServerTimer, "AUTO_RESTART_SERVER_TIMER", "0") - isSSCMEnabledVal := getBool(cfg.IsSSCMEnabled, "IS_SSCM_ENABLED", true) + + isBepInExEnabledVal := getBool(cfg.IsBepInExEnabled, "IS_BEPINEX_ENABLED", false) + IsBepInExEnabled = isBepInExEnabledVal + cfg.IsBepInExEnabled = &isBepInExEnabledVal + + isSSCMEnabledVal := getBool(cfg.IsSSCMEnabled, "IS_SSCM_ENABLED", false) IsSSCMEnabled = isSSCMEnabledVal cfg.IsSSCMEnabled = &isSSCMEnabledVal @@ -368,6 +374,7 @@ func safeSaveConfig() error { SubsystemFilters: SubsystemFilters, IsUpdateEnabled: &IsUpdateEnabled, IsSSCMEnabled: &IsSSCMEnabled, + IsBepInExEnabled: &IsBepInExEnabled, AutoRestartServerTimer: AutoRestartServerTimer, AllowPrereleaseUpdates: &AllowPrereleaseUpdates, AllowMajorUpdates: &AllowMajorUpdates, diff --git a/src/config/getters.go b/src/config/getters.go index 31ea5ca8..80b57d0b 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -327,6 +327,12 @@ func GetIsSSCMEnabled() bool { return IsSSCMEnabled } +func GetIsBepInExEnabled() bool { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return IsBepInExEnabled +} + func GetSSCMFilePath() string { ConfigMu.RLock() defer ConfigMu.RUnlock() @@ -526,13 +532,6 @@ func GetIsStationeersMode() bool { return IsStationeersMode } -// GetIsSteamServerUI returns if the system is in SteamServerUI mode -func GetUseRunfiles() bool { - ConfigMu.Lock() - defer ConfigMu.Unlock() - return UseRunfiles -} - // GetRunFilesFolder returns the RunFilesFolder func GetRunFilesFolder() string { ConfigMu.Lock() diff --git a/src/config/setters.go b/src/config/setters.go index 0f886bd2..50a42b9b 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -30,6 +30,14 @@ func SetIsSSCMEnabled(value bool) error { return safeSaveConfig() } +func SetIsBepInExEnabled(value bool) error { + ConfigMu.Lock() + defer ConfigMu.Unlock() + + IsBepInExEnabled = value + return safeSaveConfig() +} + func SetCurrentBranchBuildID(value string) error { ConfigMu.Lock() defer ConfigMu.Unlock() diff --git a/src/config/vars.go b/src/config/vars.go index 6b9d8f0d..3b75400a 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -128,10 +128,11 @@ var ( AllowAutoGameServerUpdates bool ) -// SSCM (Stationeers Server Command Manager) settings +// BepInEx settings var ( - IsSSCMEnabled bool + IsSSCMEnabled bool + IsBepInExEnabled bool ) // File paths diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index bf6a2b77..b042b6a4 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -24,7 +24,7 @@ func InitBackend(wg *sync.WaitGroup) { defer wg.Done() ReloadConfig() ReloadRunfile() - ReloadStationeersSSCM() + ReloadBepInEx() ReloadStationeersBackupManager() ReloadLocalizer() ReloadAppInfoPoller() @@ -37,7 +37,7 @@ func ReloadBackend() { logger.Core.Info("Reloading backend...") ReloadConfig() - ReloadStationeersSSCM() + ReloadBepInEx() ReloadStationeersBackupManager() ReloadLocalizer() ReloadAppInfoPoller() @@ -55,10 +55,11 @@ func ReloadConfig() { } -func ReloadStationeersSSCM() { - if config.GetIsStationeersMode() { +func ReloadBepInEx() { + if config.GetIsBepInExEnabled() { + setup.CheckAndInstallBepInEx() if config.GetIsSSCMEnabled() { - setup.InstallSSCM() + setup.CheckAndDownloadSSCM() } } } diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 7357bf05..5af0c2d2 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -36,10 +36,6 @@ func InitRunfile(game string) error { } func ReloadRunfile() error { - if !config.GetUseRunfiles() { - logger.Runfile.Warn("Runfile reloading is only supported in SteamServerUI mode") - return nil - } if err := runfile.LoadRunfile(config.GetRunfileIdentifier(), config.GetRunFilesFolder()); err != nil { logger.Runfile.Warn("Failed to reload runfile: " + err.Error()) diff --git a/src/managers/gamemgr/args.go b/src/managers/gamemgr/args.go deleted file mode 100644 index 3ccba137..00000000 --- a/src/managers/gamemgr/args.go +++ /dev/null @@ -1,118 +0,0 @@ -package gamemgr - -import ( - "runtime" - "strconv" - "strings" - - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" -) - -type Arg struct { - Flag string - Value string - RequiresValue bool - Condition func() bool - NoQuote bool -} - -func buildCommandArgs() []string { - var argOrder []Arg - - if config.IsNewTerrainAndSaveSystem { - argOrder = []Arg{ - {Flag: "-nographics", RequiresValue: false}, - {Flag: "-batchmode", RequiresValue: false}, - /* file start: (expects up to four optional args for: - -worldid (Optional to LOAD, required to CREATE save, if start value is not found, tries to create map with worldid) -> config.BackupWorldName for legacy reasons - -difficulty (Optional, defaults to "Normal" if not provided) - -startcondition (Optional, defaults to the default start condition for the world setting if not provided.) - -startlocation (Optional, defaults to "DefaultStartLocation" if not provided.) - */ - {Flag: "-file", RequiresValue: false}, - {Flag: "start", Value: config.GetSaveName(), RequiresValue: true}, - {Flag: config.GetWorldID(), RequiresValue: false}, - {Flag: config.GetDifficulty(), RequiresValue: false, Condition: func() bool { return config.GetDifficulty() != "" }}, - {Flag: config.GetStartCondition(), RequiresValue: false, Condition: func() bool { return config.GetStartCondition() != "" }}, - {Flag: config.GetStartLocation(), RequiresValue: false, Condition: func() bool { return config.GetStartLocation() != "" }}, - // file start end - {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, - {Flag: "-settings", RequiresValue: false}, - {Flag: "StartLocalHost", Value: strconv.FormatBool(config.GetStartLocalHost()), RequiresValue: true}, - {Flag: "ServerVisible", Value: strconv.FormatBool(config.GetServerVisible()), RequiresValue: true}, - {Flag: "GamePort", Value: config.GetGamePort(), RequiresValue: true}, - {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.GetUPNPEnabled()), RequiresValue: true}, - {Flag: "ServerName", Value: config.GetServerName(), RequiresValue: true}, - {Flag: "ServerPassword", Value: config.GetServerPassword(), Condition: func() bool { return config.GetServerPassword() != "" }, RequiresValue: true}, - {Flag: "ServerMaxPlayers", Value: config.GetServerMaxPlayers(), RequiresValue: true}, - {Flag: "AutoSave", Value: strconv.FormatBool(config.GetAutoSave()), RequiresValue: true}, - {Flag: "SaveInterval", Value: config.GetSaveInterval(), RequiresValue: true}, - {Flag: "ServerAuthSecret", Value: config.GetServerAuthSecret(), Condition: func() bool { return config.GetServerAuthSecret() != "" }, RequiresValue: true}, - {Flag: "UpdatePort", Value: config.GetUpdatePort(), RequiresValue: true}, - {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.GetAutoPauseServer()), RequiresValue: true}, - {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.GetUseSteamP2P()), RequiresValue: true}, - {Flag: "AdminPassword", Value: config.GetAdminPassword(), Condition: func() bool { return config.GetAdminPassword() != "" }, RequiresValue: true}, - } - } - if !config.GetIsNewTerrainAndSaveSystem() { - argOrder = []Arg{ - {Flag: "-nographics", RequiresValue: false}, - {Flag: "-batchmode", RequiresValue: false}, - {Flag: "-LOAD", Value: config.GetLegacySaveInfo(), RequiresValue: true, NoQuote: true}, // LOAD has special handling because the gameserver expects 2 parameters - {Flag: "-logFile", Value: "./debug.log", Condition: func() bool { return runtime.GOOS == "linux" }, RequiresValue: true}, - {Flag: "-settings", RequiresValue: false}, - {Flag: "StartLocalHost", Value: strconv.FormatBool(config.GetStartLocalHost()), RequiresValue: true}, - {Flag: "ServerVisible", Value: strconv.FormatBool(config.GetServerVisible()), RequiresValue: true}, - {Flag: "GamePort", Value: config.GetGamePort(), RequiresValue: true}, - {Flag: "UPNPEnabled", Value: strconv.FormatBool(config.GetUPNPEnabled()), RequiresValue: true}, - {Flag: "ServerName", Value: config.GetServerName(), RequiresValue: true}, - {Flag: "ServerPassword", Value: config.GetServerPassword(), Condition: func() bool { return config.GetServerPassword() != "" }, RequiresValue: true}, - {Flag: "ServerMaxPlayers", Value: config.GetServerMaxPlayers(), RequiresValue: true}, - {Flag: "AutoSave", Value: strconv.FormatBool(config.GetAutoSave()), RequiresValue: true}, - {Flag: "SaveInterval", Value: config.GetSaveInterval(), RequiresValue: true}, - {Flag: "ServerAuthSecret", Value: config.GetServerAuthSecret(), Condition: func() bool { return config.GetServerAuthSecret() != "" }, RequiresValue: true}, - {Flag: "UpdatePort", Value: config.GetUpdatePort(), RequiresValue: true}, - {Flag: "AutoPauseServer", Value: strconv.FormatBool(config.GetAutoPauseServer()), RequiresValue: true}, - {Flag: "UseSteamP2P", Value: strconv.FormatBool(config.GetUseSteamP2P()), RequiresValue: true}, - {Flag: "AdminPassword", Value: config.GetAdminPassword(), Condition: func() bool { return config.GetAdminPassword() != "" }, RequiresValue: true}, - } - } - - var args []string - for _, arg := range argOrder { - if arg.Condition != nil && !arg.Condition() { - continue - } - if arg.RequiresValue && arg.Value == "" { - continue - } - - args = append(args, arg.Flag) - - // handling of Legacy SaveInfo: Split on semicolon and add each part as a separate arg. This is a hack to continue to support the old saveinfo format for preterrain servers. - if arg.Flag == "-LOAD" && arg.Value != "" { - parts := strings.SplitN(arg.Value, ";", 2) - for _, part := range parts { - if part != "" { - args = append(args, part) - } - } - continue - } - - if arg.Value != "" { - args = append(args, arg.Value) - } - } - - if config.GetAdditionalParams() != "" { - args = append(args, strings.Fields(config.GetAdditionalParams())...) - } - - if config.GetLocalIpAddress() != "" { - args = append(args, "LocalIpAddress") - args = append(args, config.GetLocalIpAddress()) - } - - return args -} diff --git a/src/managers/gamemgr/bepinex.go b/src/managers/gamemgr/bepinex.go index 081e8bcf..7a66aa4b 100644 --- a/src/managers/gamemgr/bepinex.go +++ b/src/managers/gamemgr/bepinex.go @@ -7,6 +7,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" ) // BepInEx version: 5.4.23.2 or v5-lts @@ -15,9 +16,13 @@ import ( // Returns a map of environment variables and an error if setup fails. func SetupBepInExEnvironment() ([]string, error) { - executablePath := config.GetExePath() + executablePath, err := runfile.CurrentRunfile.GetExecutable() + if err != nil { + logger.Core.Error("Failed to get executable path from runfile for Bepinex setup: " + err.Error()) + return nil, err + } - if !config.GetIsSSCMEnabled() { + if !config.GetIsBepInExEnabled() { logger.Core.Debug("SSCM is disabled, skipping environment setup") return nil, nil } diff --git a/src/managers/gamemgr/processmanagement.go b/src/managers/gamemgr/processmanagement.go index 1f04fe71..6217fd80 100644 --- a/src/managers/gamemgr/processmanagement.go +++ b/src/managers/gamemgr/processmanagement.go @@ -21,7 +21,6 @@ var ( cmd *exec.Cmd mu sync.Mutex logDone chan struct{} - err error processExited chan struct{} // autoRestartDone is defined in autorestart.go ) @@ -34,15 +33,22 @@ func InternalStartServer() error { return fmt.Errorf("server is already running") } - args := buildCommandArgs() + args, err := runfile.BuildCommandArgs() + if err != nil { + logger.Core.Error("Failed to build command args: " + err.Error()) + return err + } - if config.UseRunfiles { - args, err = runfile.BuildCommandArgs() + executable, err := runfile.CurrentRunfile.GetExecutable() + if err != nil { + logger.Core.Error("Failed to get executable path from runfile: " + err.Error()) + return err } logger.Core.Info("=== GAMESERVER STARTING ===") + logger.Core.Info("BepInEx/Doorstop enabled: " + strconv.FormatBool(config.GetIsBepInExEnabled())) - if config.GetIsSSCMEnabled() && runtime.GOOS == "linux" { + if config.GetIsBepInExEnabled() && runtime.GOOS == "linux" { var envVars []string // Set up SSCM (BepInEx/Doorstop) environment @@ -51,28 +57,44 @@ func InternalStartServer() error { return fmt.Errorf("failed to set up SSCM environment: %v", err) } // Create command after environment is set - cmd = exec.Command(config.GetExePath(), args...) + cmd = exec.Command(executable, args...) // Set the environment for the command if envVars != nil { cmd.Env = envVars logger.Core.Info("BepInEx/Doorstop environment configured for server process") } - logger.Core.Info("• Executable: " + config.GetExePath() + " (with SSCM)") - logger.Core.Info("• Arguments: " + strings.Join(args, " ")) + logger.Core.Info("• Executable: " + executable) + var formattedArgs []string + for _, arg := range args { + if strings.ContainsAny(arg, " \t\n\"'") { + formattedArgs = append(formattedArgs, `"`+strings.ReplaceAll(arg, `"`, `\"`)+`"`) + } else { + formattedArgs = append(formattedArgs, arg) + } + } + logger.Core.Info("• Arguments: " + strings.Join(formattedArgs, " ")) } - if !config.GetIsSSCMEnabled() && runtime.GOOS == "linux" { + if !config.GetIsBepInExEnabled() && runtime.GOOS == "linux" { // Use ExePath directly as the command - cmd = exec.Command(config.GetExePath(), args...) - logger.Core.Info("• Executable: " + config.GetExePath()) - logger.Core.Info("• Arguments: " + strings.Join(args, " ")) + cmd = exec.Command(executable, args...) + logger.Core.Info("• Executable: " + executable) + var formattedArgs []string + for _, arg := range args { + if strings.ContainsAny(arg, " \t\n\"'") { + formattedArgs = append(formattedArgs, `"`+strings.ReplaceAll(arg, `"`, `\"`)+`"`) + } else { + formattedArgs = append(formattedArgs, arg) + } + } + logger.Core.Info("• Arguments: " + strings.Join(formattedArgs, " ")) } if runtime.GOOS == "windows" { // On Windows, set the command to use the executable path and arguments - cmd = exec.Command(config.GetExePath(), args...) - logger.Core.Info("• Executable: " + config.GetExePath()) + cmd = exec.Command(executable, args...) + logger.Core.Info("• Executable: " + executable) logger.Core.Debug("Switching to pipes for logs as we are on Windows!") stdout, err := cmd.StdoutPipe() diff --git a/src/setup/sscm.go b/src/setup/bepinex.go similarity index 95% rename from src/setup/sscm.go rename to src/setup/bepinex.go index d278fa97..ce2d185e 100644 --- a/src/setup/sscm.go +++ b/src/setup/bepinex.go @@ -161,15 +161,3 @@ func downloadAndInstallBepInEx(url string) error { return nil } - -func InstallSSCM() { - logger.Install.Info("🕑Installing SSCM...") - - CheckAndInstallBepInEx() - CheckAndDownloadSSCM() - - // Enable SSCM - config.SetIsSSCMEnabled(true) - - logger.Install.Info("✅SSCM enabled") -} diff --git a/src/setup/install.go b/src/setup/install.go index 2dcb07ee..5c25cd95 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -17,7 +17,6 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" ) var downloadBranch string // Holds the branch to download from @@ -40,13 +39,6 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("🔄Checking for Blacklist.txt...") checkAndCreateBlacklist() logger.Install.Info("✅Blacklist.txt verified or created.") - // Step 3: Install and run SteamCMD - logger.Install.Info("🔄Installing and running SteamCMD...") - if config.GetSkipSteamCMD() || config.GetUseRunfiles() { - logger.Install.Info("✅Skipping SteamCMD installation, SkipSteamCMD is true or IsSteamServerUI is true") - } else { - steamcmd.InstallAndRunSteamCMD() - } logger.Install.Info("✅Setup complete!") } diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index 57be19fe..ab47bda6 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -86,11 +86,7 @@ func getAppInfo() error { steamcmddir := SteamCMDLinuxDir executable := "steamcmd.sh" - appid := config.GetGameServerAppID() - - if config.GetUseRunfiles() { - appid = runfile.CurrentRunfile.SteamAppID - } + appid := runfile.CurrentRunfile.SteamAppID if runtime.GOOS == "windows" { executable = "steamcmd.exe" diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index d72fcc6e..ce6c4aa1 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -145,17 +145,10 @@ func runSteamCMD(steamCMDDir string) (int, error) { // buildSteamCMDCommand constructs the SteamCMD command based on the OS. func buildSteamCMDCommand(steamCMDDir, currentDir string) *exec.Cmd { //print the config.GameBranch and config.GameServerAppID - steamAppID := config.GetGameServerAppID() - if config.GetUseRunfiles() { - logger.Install.Info("🔍 SSUI Runfile Identifier: " + runfile.CurrentRunfile.Meta.Name) - logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) - logger.Install.Info("🔍 Game Server App ID: " + runfile.CurrentRunfile.SteamAppID) - steamAppID = runfile.CurrentRunfile.SteamAppID - } else { - logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) - logger.Install.Info("🔍 Game Server App ID: " + config.GetGameServerAppID()) - - } + logger.Install.Info("🔍 SSUI Runfile Identifier: " + runfile.CurrentRunfile.Meta.Name) + logger.Install.Info("🔍 Game Branch: " + config.GetGameBranch()) + logger.Install.Info("🔍 Game Server App ID: " + runfile.CurrentRunfile.SteamAppID) + steamAppID := runfile.CurrentRunfile.SteamAppID if runtime.GOOS == "windows" { return exec.Command(filepath.Join(steamCMDDir, "steamcmd.exe"), "+force_install_dir", currentDir, "+login", "anonymous", "+app_update", steamAppID, "-beta", config.GetGameBranch(), "validate", "+quit") From e38fddb52f25a12c1aa64d86db89ab1accd679c7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:01:03 +0200 Subject: [PATCH 27/93] allow any category name from the rf on the UI --- src/steamserverui/runfile/args.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index 1b77445c..9bda8eec 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -139,10 +139,8 @@ func (rf *RunFile) Validate() error { // getAllArgs returns all GameArgs (internal method for validation) func (rf *RunFile) getAllArgs() []GameArg { var allArgs []GameArg - for _, category := range []string{"basic", "network", "advanced"} { - if args, exists := rf.Args[category]; exists { - allArgs = append(allArgs, args...) - } + for _, args := range rf.Args { + allArgs = append(allArgs, args...) } return allArgs } @@ -377,13 +375,14 @@ func BuildCommandArgs() ([]string, error) { } // switchCategoryWeight maps UIGroup to a weight for sorting +// not functioning as intended, order is not consistent func switchCategoryWeight(group string) int { switch group { case "Basic": return 0 case "Network": return 1 - case "Advanced": + case "Special": return 2 default: return 3 From c14339562ae50066203a6e9498f66d68f790aebb Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:02:09 +0200 Subject: [PATCH 28/93] Added socket server: Unix socket / named pipes and add dedicated logging for socket operations --- go.mod | 1 + go.sum | 2 + server.go | 3 + src/logger/logger.go | 3 + src/web/socket/examples.md | 126 +++++++++++++++++++++++++++++++++++ src/web/socket/socket-lin.go | 69 +++++++++++++++++++ src/web/socket/socket-win.go | 58 ++++++++++++++++ src/web/start.go | 6 +- 8 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 src/web/socket/examples.md create mode 100644 src/web/socket/socket-lin.go create mode 100644 src/web/socket/socket-win.go diff --git a/go.mod b/go.mod index 679eae5c..66419a87 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/jacksonthemaster/discordrichpresence v1.1.0 + github.com/microsoft/go-winio v0.4.12 golang.org/x/crypto v0.37.0 golang.org/x/sys v0.35.0 ) diff --git a/go.sum b/go.sum index 870eb11e..1be18036 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jacksonthemaster/discordrichpresence v1.1.0 h1:4UmompqAKyEpspR/Z0LFj6vdSWJf6FZQ/J787KvdxMM= github.com/jacksonthemaster/discordrichpresence v1.1.0/go.mod h1:XA0SB8bsEc5oJCQcXjC78BfNBj9FoLFA4ysfevkUjHE= +github.com/microsoft/go-winio v0.4.12 h1:3vDRRsUnj2dKE7QKoedntu9hbuD8gzaVd2E2UZioqx4= +github.com/microsoft/go-winio v0.4.12/go.mod h1:kcIxxtKZE55DEncT/EOvFiygPobhUWpSDqDb47poQOU= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= diff --git a/server.go b/server.go index 94cb1576..43d6838d 100644 --- a/server.go +++ b/server.go @@ -29,6 +29,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web/socket" ) //go:embed UIMod/onboard_bundled @@ -56,6 +57,8 @@ func main() { wg.Wait() logger.Main.Debug("Starting webserver...") web.StartWebServer(&wg) + logger.Main.Debug("Starting socket server...") + socket.StartSocketServer(&wg) logger.Main.Debug("Initializing SSUICLI...") cli.StartConsole(&wg) wg.Wait() diff --git a/src/logger/logger.go b/src/logger/logger.go index 96ed57eb..0c888f27 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -25,6 +25,7 @@ var ( Security = &Logger{suffix: SYS_SECURITY} Localization = &Logger{suffix: SYS_LOCALIZATION} Runfile = &Logger{suffix: SYS_RUNFILE} + Socket = &Logger{suffix: SYS_SOCKET} ) // Severity Levels @@ -50,6 +51,7 @@ const ( SYS_SECURITY = "SECURITY" SYS_LOCALIZATION = "LOCALIZATION" SYS_RUNFILE = "RUNFILE" + SYS_SOCKET = "SOCKET" ) const ( @@ -75,6 +77,7 @@ var subsystemColors = map[string]string{ SYS_SSE: colorCyan, // Matches WEB, streaming vibe SYS_SECURITY: colorRed, // Screams "pay attention" SYS_LOCALIZATION: colorCyan, // Matches WEB, localization-related + SYS_SOCKET: colorCyan, // Matches WEB, socket-related } // Global channels and mutex for all loggers diff --git a/src/web/socket/examples.md b/src/web/socket/examples.md new file mode 100644 index 00000000..455b8b37 --- /dev/null +++ b/src/web/socket/examples.md @@ -0,0 +1,126 @@ +# Socket Server Usage + +This guide explains how to interact with the socket-based `SSUI-API`, which exposes the same HTTP endpoints as the http server over a _Unix socket on Linux_ or a _named pipe on Windows_. This allows local, lightweight access to the API without network overhead or authentication, mainly for the planned plugin system but also for advancedscripting or external local tools. The server is built with Go’s standard library on Linux and `microsoft/go-winio` on Windows for cross-platform compatibility. + +## Overview + +The socket server reuses the HTTP routes defined in `routes.go` but serves them over: +- **Linux**: A Unix socket at `/tmp/ssui-api.sock`. +- **Windows**: A named pipe at `\\.\pipe\ssui-api`. + +This is similar to how Docker uses `/var/run/docker.sock` for local API access. + +## Prerequisites + +- **Linux**: + - `curl` installed for testing. +- **Windows**: + - PowerShell 7+ for testing. + +## Testing the Socket Server + +### Linux: Using a Unix Socket + +Use `curl` with the `--unix-socket` flag to send HTTP requests to the socket. Example for the `/api/v2/settings` endpoint: + +```bash +curl --unix-socket /tmp/ssui-api.sock http://localhost/api/v2/settings +``` + +**Expected Output**: JSON response from the `settings.RetrieveSettings` handler, e.g.: +```json +{ + "settings": { + "some_key": "some_value" + } +} +``` + +### Windows: Using a Named Pipe + +Use the following PowerShell 7 script to send an HTTP request to the named pipe. Save as `test_namedpipe.ps1` and run it. + +```powershell +# test_namedpipe.ps1 + +$pipeName = "\\.\pipe\ssui-api" +$endpoint = "/api/v2/server/status" +$hostHeader = "localhost" # Dummy host for HTTP request + +# Create the HTTP request +$request = "GET $endpoint HTTP/1.1`r`nHost: $hostHeader`r`nConnection: close`r`n`r`n" + +try { + # Connect to the named pipe + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", "ssui-api", [System.IO.Pipes.PipeDirection]::InOut) + $pipe.Connect(5000) # 5-second timeout + + # Convert request to bytes and send + $requestBytes = [System.Text.Encoding]::UTF8.GetBytes($request) + $pipe.Write($requestBytes, 0, $requestBytes.Length) + $pipe.Flush() + + # Read response + $reader = New-Object System.IO.StreamReader($pipe) + $response = "" + while ($null -ne ($line = $reader.ReadLine())) { + $response += "$line`n" + } + + # Output the response + Write-Output $response +} +catch { + Write-Error "Error connecting to named pipe or reading response: $_" +} +finally { + # Clean up + if ($null -ne $pipe) { + $pipe.Close() + $pipe.Dispose() + } + if ($null -ne $reader) { + $reader.Close() + $reader.Dispose() + } +} +``` + +Run it: +```powershell +.\test_namedpipe.ps1 +``` + +**Expected Output**: HTTP response with headers and JSON body, e.g.: +``` +Response from \\.\pipe\ssui-api/api/v2/server/status: +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 123 + +{"status":"running","players":0} +``` + +## Testing Other Endpoints + +All routes from `routes.go` (e.g., `/api/v2/server/start`, `/api/v2/backups`) are available. Modify the endpoint in the commands: + +- **Linux**: + ```bash + curl --unix-socket /tmp/ssui-api.sock http://localhost/api/v2/backups + ``` +- **Windows**: Edit `$endpoint` in `test_namedpipe.ps1`, e.g., `$endpoint = "/api/v2/backups"`. + +For POST requests (e.g., `/api/v2/server/start`), add a JSON payload: +- **Linux**: + ```bash + curl --unix-socket /tmp/ssui-api.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start + ``` +- **Windows**: Update the PowerShell script’s `$request`: + ```powershell + $body = '{"action":"start"}' + $request = "POST $endpoint HTTP/1.1`r`nHost: $hostHeader`r`nContent-Type: application/json`r`nContent-Length: $($body.Length)`r`n`r`n$body" + ``` + +## Notes +- The socket server skips authentication, assuming local access is secure (like Docker’s socket). \ No newline at end of file diff --git a/src/web/socket/socket-lin.go b/src/web/socket/socket-lin.go new file mode 100644 index 00000000..c702c98e --- /dev/null +++ b/src/web/socket/socket-lin.go @@ -0,0 +1,69 @@ +//go:build linux +// +build linux + +package socket + +import ( + "context" + "log" + "net" + "net/http" + "os" + "sync" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" +) + +const socketPath = "/tmp/ssui-api.sock" + +func StartSocketServer(wg *sync.WaitGroup) { + logger.Socket.Info("Starting Unix socket server...") + + // Remove existing socket file if it exists + if err := os.RemoveAll(socketPath); err != nil { + logger.Socket.Error("Error removing existing socket: " + err.Error()) + } + + // Set up routes + mux, protectedMux := web.SetupRoutes() + mux.Handle("/", protectedMux) + + // Create Unix socket listener + listener, err := net.Listen("unix", socketPath) + if err != nil { + logger.Socket.Error("Error starting Unix socket server: " + err.Error()) + return + } + + // Set socket permissions + if err := os.Chmod(socketPath, 0666); err != nil { + logger.Socket.Error("Error setting socket permissions: " + err.Error()) + } + + // Create HTTP server + server := &http.Server{ + Handler: mux, + ErrorLog: log.New(&web.WebServerLogger{}, "", 0), + } + + // Start server in a goroutine + wg.Go(func() { + logger.Socket.Info("Unix socket server running at " + socketPath) + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + logger.Socket.Error("Unix socket server error: " + err.Error()) + } + }) + + // Handle graceful shutdown + go func() { + <-context.Background().Done() + logger.Socket.Info("Shutting down Unix socket server...") + if err := server.Shutdown(context.Background()); err != nil { + logger.Socket.Error("Error shutting down Unix socket server: " + err.Error()) + } + if err := os.RemoveAll(socketPath); err != nil { + logger.Socket.Error("Error removing socket file: " + err.Error()) + } + }() +} diff --git a/src/web/socket/socket-win.go b/src/web/socket/socket-win.go new file mode 100644 index 00000000..ae037f9e --- /dev/null +++ b/src/web/socket/socket-win.go @@ -0,0 +1,58 @@ +//go:build windows +// +build windows + +package socket + +import ( + "context" + "log" + "net/http" + "sync" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" + "github.com/microsoft/go-winio" +) + +const pipePath = `\\.\pipe\ssui-api` + +func StartSocketServer(wg *sync.WaitGroup) { + logger.Socket.Info("Starting named pipe server...") + + // Set up routes + mux, protectedMux := web.SetupRoutes() + mux.Handle("/", protectedMux) + + // Create named pipe listener + listener, err := winio.ListenPipe(pipePath, nil) + if err != nil { + logger.Socket.Error("Error starting named pipe server: " + err.Error()) + return + } + + // Create HTTP server + server := &http.Server{ + Handler: mux, + ErrorLog: log.New(&web.WebServerLogger{}, "", 0), + } + + // Start server in a goroutine + wg.Add(1) + go func() { + defer wg.Done() + logger.Socket.Info("Named pipe server running at " + pipePath) + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + logger.Socket.Error("Named pipe server error: " + err.Error()) + } + }() + + // Handle graceful shutdown + go func() { + <-context.Background().Done() + logger.Socket.Info("Shutting down named pipe server...") + if err := server.Shutdown(context.Background()); err != nil { + logger.Socket.Error("Error shutting down named pipe server: " + err.Error()) + } + listener.Close() // Ensure the pipe is closed + }() +} diff --git a/src/web/start.go b/src/web/start.go index 3b52b85b..4360571e 100644 --- a/src/web/start.go +++ b/src/web/start.go @@ -12,9 +12,9 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) -type webServerLogger struct{} +type WebServerLogger struct{} -func (cl *webServerLogger) Write(p []byte) (n int, err error) { +func (cl *WebServerLogger) Write(p []byte) (n int, err error) { // Redirect HTTP server logs (like TLS handshake errors) to logger.Web.Debug logger.Web.Debug(string(p)) return len(p), nil @@ -28,7 +28,7 @@ func StartWebServer(wg *sync.WaitGroup) { // Apply middleware only to protected routes mux.Handle("/", AuthMiddleware(protectedMux)) // Wrap protected routes under root - httpLogger := log.New(&webServerLogger{}, "", 0) + httpLogger := log.New(&WebServerLogger{}, "", 0) // Start HTTP server wg.Add(1) go func() { From 4238c9089c077fed9d611a0f40903e733e7af0d9 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:02:49 +0200 Subject: [PATCH 29/93] add runfiles folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ecfce688..afe048f0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ frontend/node_modules frontend/dist frontend/build UIMod/onboard_bundled/v2 +UIMod/runfiles/** From 58850f0200d0077c6abd9bdf6ac88053a6afc8fc Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 01:05:25 +0200 Subject: [PATCH 30/93] updated logger references in future api package to use logger.API --- src/logger/logger.go | 3 +++ src/managers/backupmgr/backuphttp.go | 2 +- src/web/http.go | 14 +++++++------- src/web/runfile.go | 2 +- src/web/start.go | 10 ++++------ src/web/svelteui.go | 2 +- src/web/systeminfo.go | 6 +++--- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/logger/logger.go b/src/logger/logger.go index 0c888f27..e6a155d8 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -15,6 +15,7 @@ import ( var ( Main = &Logger{suffix: SYS_MAIN} Web = &Logger{suffix: SYS_WEB} + API = &Logger{suffix: SYS_API} Discord = &Logger{suffix: SYS_DISCORD} Backup = &Logger{suffix: SYS_BACKUP} Detection = &Logger{suffix: SYS_DETECT} @@ -41,6 +42,7 @@ const ( const ( SYS_MAIN = "MAIN" SYS_WEB = "WEB" + SYS_API = "API" SYS_DISCORD = "DISCORD" SYS_BACKUP = "BACKUP" SYS_DETECT = "DETECT" @@ -68,6 +70,7 @@ const ( var subsystemColors = map[string]string{ SYS_MAIN: colorBlue, // Calm, default system SYS_WEB: colorCyan, // Clean, UI-related + SYS_API: colorCyan, // Matches WEB SYS_DISCORD: colorMagenta, // Flashy, chatty subsystem SYS_BACKUP: colorGreen, // Safe, reliable vibe SYS_DETECT: colorYellow, // Attention-grabbing for detection diff --git a/src/managers/backupmgr/backuphttp.go b/src/managers/backupmgr/backuphttp.go index 8ec322d4..729cc1df 100644 --- a/src/managers/backupmgr/backuphttp.go +++ b/src/managers/backupmgr/backuphttp.go @@ -68,7 +68,7 @@ func (h *HTTPHandler) ListBackupsHandler(w http.ResponseWriter, r *http.Request) // RestoreBackupHandler handles requests to restore a backup func (h *HTTPHandler) RestoreBackupHandler(w http.ResponseWriter, r *http.Request) { - logger.Web.Debug("Received restore request") + logger.API.Debug("Received restore request") indexStr := r.URL.Query().Get("index") if indexStr == "" { http.Error(w, "index parameter is required", http.StatusBadRequest) diff --git a/src/web/http.go b/src/web/http.go index fd010f65..c290912e 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -19,32 +19,32 @@ import ( // StartServer HTTP handler func StartServer(w http.ResponseWriter, r *http.Request) { - logger.Web.Debug("Received start request from API") + logger.API.Debug("Received start request from API") if err := gamemgr.InternalStartServer(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - logger.Web.Error("Error starting server: " + err.Error()) + logger.API.Error("Error starting server: " + err.Error()) return } fmt.Fprint(w, localization.GetString("BackendText_ServerStarted")) - logger.Web.Info("Server started.") + logger.API.Info("Server started.") } // StopServer HTTP handler func StopServer(w http.ResponseWriter, r *http.Request) { - logger.Web.Debug("Received stop request from API") + logger.API.Debug("Received stop request from API") if err := gamemgr.InternalStopServer(); err != nil { if err.Error() == "server not running" { fmt.Fprint(w, localization.GetString("BackendText_ServerNotRunningOrAlreadyStopped")) - logger.Web.Warn("Server not running or was already stopped") + logger.API.Warn("Server not running or was already stopped") return } http.Error(w, err.Error(), http.StatusInternalServerError) - logger.Web.Error("Error stopping server: " + err.Error()) + logger.API.Error("Error stopping server: " + err.Error()) return } detectionmgr.ClearPlayers(detectionmgr.GetDetector()) fmt.Fprint(w, localization.GetString("BackendText_ServerStopped")) - logger.Web.Info("Server stopped.") + logger.API.Info("Server stopped.") } func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { diff --git a/src/web/runfile.go b/src/web/runfile.go index 27ff62cf..bf9a7e18 100644 --- a/src/web/runfile.go +++ b/src/web/runfile.go @@ -286,7 +286,7 @@ func HandleSetRunfileGame(w http.ResponseWriter, r *http.Request) { } func HandleReloadRunfile(w http.ResponseWriter, r *http.Request) { - logger.Web.Debug("Received reloadrunfile request from API") + logger.API.Debug("Received reloadrunfile request from API") reloadMu.Lock() defer reloadMu.Unlock() // accept only GET requests diff --git a/src/web/start.go b/src/web/start.go index 4360571e..721bc09f 100644 --- a/src/web/start.go +++ b/src/web/start.go @@ -15,8 +15,8 @@ import ( type WebServerLogger struct{} func (cl *WebServerLogger) Write(p []byte) (n int, err error) { - // Redirect HTTP server logs (like TLS handshake errors) to logger.Web.Debug - logger.Web.Debug(string(p)) + // Redirect HTTP server logs (like TLS handshake errors) to logger.API.Debug + logger.API.Debug(string(p)) return len(p), nil } @@ -54,9 +54,7 @@ func StartWebServer(wg *sync.WaitGroup) { // Start the pprof server if debug mode is enabled (HTTP/1.1) if config.GetIsDebugMode() { // if debug mode is enabled, start pprof server - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { pprofMux := http.NewServeMux() // Register pprof handler pprofMux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) @@ -65,6 +63,6 @@ func StartWebServer(wg *sync.WaitGroup) { if err != nil { logger.Web.Error("Error starting pprof server: " + err.Error()) } - }() + }) } } diff --git a/src/web/svelteui.go b/src/web/svelteui.go index c563df63..db0201ce 100644 --- a/src/web/svelteui.go +++ b/src/web/svelteui.go @@ -38,7 +38,7 @@ func ServeSvelteUI(w http.ResponseWriter, r *http.Request) { } func HandleReloadAll(w http.ResponseWriter, r *http.Request) { - logger.Web.Debug("Received reloadbackend request from API") + logger.API.Debug("Received reloadbackend request from API") reloadMu.Lock() defer reloadMu.Unlock() // accept only GET requests diff --git a/src/web/systeminfo.go b/src/web/systeminfo.go index ccb52e99..c02e4b41 100644 --- a/src/web/systeminfo.go +++ b/src/web/systeminfo.go @@ -9,7 +9,7 @@ import ( ) func HandleGetOsStats(w http.ResponseWriter, r *http.Request) { - logger.Web.Debug("Received getOsStats request from API") + logger.API.Debug("Received getOsStats request from API") // accept only GET requests if r.Method != http.MethodGet { http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) @@ -19,7 +19,7 @@ func HandleGetOsStats(w http.ResponseWriter, r *http.Request) { // Get cached stats or refresh if needed stats, err := systeminfo.RefreshCachedStats() if err != nil { - logger.Web.Error("Failed to get OS stats") + logger.API.Error("Failed to get OS stats") http.Error(w, "Failed to get OS stats", http.StatusInternalServerError) return } @@ -28,7 +28,7 @@ func HandleGetOsStats(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(stats); err != nil { - logger.Web.Error("Failed to write response") + logger.API.Error("Failed to write response") http.Error(w, "Failed to write response", http.StatusInternalServerError) return } From 11bb32b0717d71f475b45b17a20253c993963229 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 01:09:46 +0200 Subject: [PATCH 31/93] renamed web package to api, changed references accordingly --- server.go | 8 ++++---- src/{web => api}/TwoBoxForm.go | 2 +- src/{web => api}/commands.go | 2 +- src/{web => api}/configpage.go | 2 +- src/{web => api}/connectedplayers.go | 2 +- src/{web => api}/detectionmanagerpage.go | 2 +- src/{web => api}/http-sse.go | 2 +- src/{web => api}/http.go | 2 +- src/{web => api}/indexpage.go | 2 +- src/{web => api}/login.go | 2 +- src/{web => api}/routes.go | 2 +- src/{web => api}/runfile.go | 2 +- src/{web => api}/runfilegallery.go | 2 +- src/{web => api}/socket/examples.md | 0 src/{web => api}/socket/socket-lin.go | 6 +++--- src/{web => api}/socket/socket-win.go | 6 +++--- src/{web => api}/start.go | 8 ++++---- src/{web => api}/svelteui.go | 2 +- src/{web => api}/systeminfo.go | 2 +- src/{web => api}/templatevars.go | 2 +- src/{web => api}/whoami.go | 2 +- 21 files changed, 30 insertions(+), 30 deletions(-) rename src/{web => api}/TwoBoxForm.go (99%) rename src/{web => api}/commands.go (99%) rename src/{web => api}/configpage.go (99%) rename src/{web => api}/connectedplayers.go (99%) rename src/{web => api}/detectionmanagerpage.go (98%) rename src/{web => api}/http-sse.go (99%) rename src/{web => api}/http.go (99%) rename src/{web => api}/indexpage.go (99%) rename src/{web => api}/login.go (99%) rename src/{web => api}/routes.go (99%) rename src/{web => api}/runfile.go (99%) rename src/{web => api}/runfilegallery.go (99%) rename src/{web => api}/socket/examples.md (100%) rename src/{web => api}/socket/socket-lin.go (91%) rename src/{web => api}/socket/socket-win.go (89%) rename src/{web => api}/start.go (91%) rename src/{web => api}/svelteui.go (99%) rename src/{web => api}/systeminfo.go (98%) rename src/{web => api}/templatevars.go (99%) rename src/{web => api}/whoami.go (97%) diff --git a/server.go b/server.go index 43d6838d..4e04bd55 100644 --- a/server.go +++ b/server.go @@ -24,12 +24,12 @@ import ( "embed" "sync" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api/socket" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/cli" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web/socket" ) //go:embed UIMod/onboard_bundled @@ -55,10 +55,10 @@ func main() { logger.Main.Debug("Initializing after start tasks...") loader.AfterStartComplete(&wg) wg.Wait() - logger.Main.Debug("Starting webserver...") - web.StartWebServer(&wg) logger.Main.Debug("Starting socket server...") socket.StartSocketServer(&wg) + logger.Main.Debug("Starting webserver...") + api.StartWebServer(&wg) logger.Main.Debug("Initializing SSUICLI...") cli.StartConsole(&wg) wg.Wait() diff --git a/src/web/TwoBoxForm.go b/src/api/TwoBoxForm.go similarity index 99% rename from src/web/TwoBoxForm.go rename to src/api/TwoBoxForm.go index c5b88773..c9bf9318 100644 --- a/src/web/TwoBoxForm.go +++ b/src/api/TwoBoxForm.go @@ -1,4 +1,4 @@ -package web +package api import ( "io/fs" diff --git a/src/web/commands.go b/src/api/commands.go similarity index 99% rename from src/web/commands.go rename to src/api/commands.go index 64a5c1c7..9b770265 100644 --- a/src/web/commands.go +++ b/src/api/commands.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/configpage.go b/src/api/configpage.go similarity index 99% rename from src/web/configpage.go rename to src/api/configpage.go index a7e77269..d52bc8f5 100644 --- a/src/web/configpage.go +++ b/src/api/configpage.go @@ -1,4 +1,4 @@ -package web +package api import ( "fmt" diff --git a/src/web/connectedplayers.go b/src/api/connectedplayers.go similarity index 99% rename from src/web/connectedplayers.go rename to src/api/connectedplayers.go index 295a8585..ccb61165 100644 --- a/src/web/connectedplayers.go +++ b/src/api/connectedplayers.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/detectionmanagerpage.go b/src/api/detectionmanagerpage.go similarity index 98% rename from src/web/detectionmanagerpage.go rename to src/api/detectionmanagerpage.go index 74f8a642..96d5f360 100644 --- a/src/web/detectionmanagerpage.go +++ b/src/api/detectionmanagerpage.go @@ -1,4 +1,4 @@ -package web +package api import ( "fmt" diff --git a/src/web/http-sse.go b/src/api/http-sse.go similarity index 99% rename from src/web/http-sse.go rename to src/api/http-sse.go index 56fc3f23..447704db 100644 --- a/src/web/http-sse.go +++ b/src/api/http-sse.go @@ -1,4 +1,4 @@ -package web +package api import ( "net/http" diff --git a/src/web/http.go b/src/api/http.go similarity index 99% rename from src/web/http.go rename to src/api/http.go index c290912e..479ef546 100644 --- a/src/web/http.go +++ b/src/api/http.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/indexpage.go b/src/api/indexpage.go similarity index 99% rename from src/web/indexpage.go rename to src/api/indexpage.go index 81d72180..f42c732a 100644 --- a/src/web/indexpage.go +++ b/src/api/indexpage.go @@ -1,4 +1,4 @@ -package web +package api import ( "io/fs" diff --git a/src/web/login.go b/src/api/login.go similarity index 99% rename from src/web/login.go rename to src/api/login.go index e2c8f0db..90b853f0 100644 --- a/src/web/login.go +++ b/src/api/login.go @@ -1,5 +1,5 @@ // handlers.go -package web +package api import ( "encoding/json" diff --git a/src/web/routes.go b/src/api/routes.go similarity index 99% rename from src/web/routes.go rename to src/api/routes.go index 7cc42aa1..f1f50c91 100644 --- a/src/web/routes.go +++ b/src/api/routes.go @@ -1,4 +1,4 @@ -package web +package api import ( "io/fs" diff --git a/src/web/runfile.go b/src/api/runfile.go similarity index 99% rename from src/web/runfile.go rename to src/api/runfile.go index bf9a7e18..2b07a538 100644 --- a/src/web/runfile.go +++ b/src/api/runfile.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/runfilegallery.go b/src/api/runfilegallery.go similarity index 99% rename from src/web/runfilegallery.go rename to src/api/runfilegallery.go index 545349c3..eff60b5d 100644 --- a/src/web/runfilegallery.go +++ b/src/api/runfilegallery.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/socket/examples.md b/src/api/socket/examples.md similarity index 100% rename from src/web/socket/examples.md rename to src/api/socket/examples.md diff --git a/src/web/socket/socket-lin.go b/src/api/socket/socket-lin.go similarity index 91% rename from src/web/socket/socket-lin.go rename to src/api/socket/socket-lin.go index c702c98e..2ca674f6 100644 --- a/src/web/socket/socket-lin.go +++ b/src/api/socket/socket-lin.go @@ -11,8 +11,8 @@ import ( "os" "sync" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" ) const socketPath = "/tmp/ssui-api.sock" @@ -26,7 +26,7 @@ func StartSocketServer(wg *sync.WaitGroup) { } // Set up routes - mux, protectedMux := web.SetupRoutes() + mux, protectedMux := api.SetupRoutes() mux.Handle("/", protectedMux) // Create Unix socket listener @@ -44,7 +44,7 @@ func StartSocketServer(wg *sync.WaitGroup) { // Create HTTP server server := &http.Server{ Handler: mux, - ErrorLog: log.New(&web.WebServerLogger{}, "", 0), + ErrorLog: log.New(&api.APIServerLogger{}, "", 0), } // Start server in a goroutine diff --git a/src/web/socket/socket-win.go b/src/api/socket/socket-win.go similarity index 89% rename from src/web/socket/socket-win.go rename to src/api/socket/socket-win.go index ae037f9e..3598b772 100644 --- a/src/web/socket/socket-win.go +++ b/src/api/socket/socket-win.go @@ -9,8 +9,8 @@ import ( "net/http" "sync" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/web" "github.com/microsoft/go-winio" ) @@ -20,7 +20,7 @@ func StartSocketServer(wg *sync.WaitGroup) { logger.Socket.Info("Starting named pipe server...") // Set up routes - mux, protectedMux := web.SetupRoutes() + mux, protectedMux := api.SetupRoutes() mux.Handle("/", protectedMux) // Create named pipe listener @@ -33,7 +33,7 @@ func StartSocketServer(wg *sync.WaitGroup) { // Create HTTP server server := &http.Server{ Handler: mux, - ErrorLog: log.New(&web.WebServerLogger{}, "", 0), + ErrorLog: log.New(&api.APIServerLogger{}, "", 0), } // Start server in a goroutine diff --git a/src/web/start.go b/src/api/start.go similarity index 91% rename from src/web/start.go rename to src/api/start.go index 721bc09f..542cff82 100644 --- a/src/web/start.go +++ b/src/api/start.go @@ -1,5 +1,5 @@ // start.go -package web +package api import ( "log" @@ -12,9 +12,9 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) -type WebServerLogger struct{} +type APIServerLogger struct{} -func (cl *WebServerLogger) Write(p []byte) (n int, err error) { +func (cl *APIServerLogger) Write(p []byte) (n int, err error) { // Redirect HTTP server logs (like TLS handshake errors) to logger.API.Debug logger.API.Debug(string(p)) return len(p), nil @@ -28,7 +28,7 @@ func StartWebServer(wg *sync.WaitGroup) { // Apply middleware only to protected routes mux.Handle("/", AuthMiddleware(protectedMux)) // Wrap protected routes under root - httpLogger := log.New(&WebServerLogger{}, "", 0) + httpLogger := log.New(&APIServerLogger{}, "", 0) // Start HTTP server wg.Add(1) go func() { diff --git a/src/web/svelteui.go b/src/api/svelteui.go similarity index 99% rename from src/web/svelteui.go rename to src/api/svelteui.go index db0201ce..548f16dc 100644 --- a/src/web/svelteui.go +++ b/src/api/svelteui.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/systeminfo.go b/src/api/systeminfo.go similarity index 98% rename from src/web/systeminfo.go rename to src/api/systeminfo.go index c02e4b41..d378c195 100644 --- a/src/web/systeminfo.go +++ b/src/api/systeminfo.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" diff --git a/src/web/templatevars.go b/src/api/templatevars.go similarity index 99% rename from src/web/templatevars.go rename to src/api/templatevars.go index 8c0cc2ab..4e0e7c62 100644 --- a/src/web/templatevars.go +++ b/src/api/templatevars.go @@ -1,4 +1,4 @@ -package web +package api // TemplateData holds data to be passed to templates type IndexTemplateData struct { diff --git a/src/web/whoami.go b/src/api/whoami.go similarity index 97% rename from src/web/whoami.go rename to src/api/whoami.go index 0baa5cc3..8b83c285 100644 --- a/src/web/whoami.go +++ b/src/api/whoami.go @@ -1,4 +1,4 @@ -package web +package api import ( "encoding/json" From 7920074b903252d70731e2a4f3aabad11ad62995 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 01:14:43 +0200 Subject: [PATCH 32/93] improve socket paths --- src/api/socket/examples.md | 20 ++++++++++---------- src/api/socket/socket-lin.go | 2 +- src/api/socket/socket-win.go | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api/socket/examples.md b/src/api/socket/examples.md index 455b8b37..f3ee2541 100644 --- a/src/api/socket/examples.md +++ b/src/api/socket/examples.md @@ -5,11 +5,14 @@ This guide explains how to interact with the socket-based `SSUI-API`, which expo ## Overview The socket server reuses the HTTP routes defined in `routes.go` but serves them over: -- **Linux**: A Unix socket at `/tmp/ssui-api.sock`. -- **Windows**: A named pipe at `\\.\pipe\ssui-api`. +- **Linux**: A Unix socket at `/var/run/ssui.sock`. +- **Windows**: A named pipe at `\\.\pipe\ssui`. This is similar to how Docker uses `/var/run/docker.sock` for local API access. +## Notes +- The socket server skips authentication, assuming local access is secure (like Docker’s socket). + ## Prerequisites - **Linux**: @@ -24,7 +27,7 @@ This is similar to how Docker uses `/var/run/docker.sock` for local API access. Use `curl` with the `--unix-socket` flag to send HTTP requests to the socket. Example for the `/api/v2/settings` endpoint: ```bash -curl --unix-socket /tmp/ssui-api.sock http://localhost/api/v2/settings +curl --unix-socket /var/run/ssui.sock http://localhost/api/v2/settings ``` **Expected Output**: JSON response from the `settings.RetrieveSettings` handler, e.g.: @@ -43,7 +46,7 @@ Use the following PowerShell 7 script to send an HTTP request to the named pipe. ```powershell # test_namedpipe.ps1 -$pipeName = "\\.\pipe\ssui-api" +$pipeName = "\\.\pipe\ssui" $endpoint = "/api/v2/server/status" $hostHeader = "localhost" # Dummy host for HTTP request @@ -93,7 +96,7 @@ Run it: **Expected Output**: HTTP response with headers and JSON body, e.g.: ``` -Response from \\.\pipe\ssui-api/api/v2/server/status: +Response from \\.\pipe\ssui/api/v2/server/status: HTTP/1.1 200 OK Content-Type: application/json Content-Length: 123 @@ -107,20 +110,17 @@ All routes from `routes.go` (e.g., `/api/v2/server/start`, `/api/v2/backups`) ar - **Linux**: ```bash - curl --unix-socket /tmp/ssui-api.sock http://localhost/api/v2/backups + curl --unix-socket /var/run/ssui.sock http://localhost/api/v2/backups ``` - **Windows**: Edit `$endpoint` in `test_namedpipe.ps1`, e.g., `$endpoint = "/api/v2/backups"`. For POST requests (e.g., `/api/v2/server/start`), add a JSON payload: - **Linux**: ```bash - curl --unix-socket /tmp/ssui-api.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start + curl --unix-socket /var/run/ssui.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start ``` - **Windows**: Update the PowerShell script’s `$request`: ```powershell $body = '{"action":"start"}' $request = "POST $endpoint HTTP/1.1`r`nHost: $hostHeader`r`nContent-Type: application/json`r`nContent-Length: $($body.Length)`r`n`r`n$body" ``` - -## Notes -- The socket server skips authentication, assuming local access is secure (like Docker’s socket). \ No newline at end of file diff --git a/src/api/socket/socket-lin.go b/src/api/socket/socket-lin.go index 2ca674f6..93a0dd80 100644 --- a/src/api/socket/socket-lin.go +++ b/src/api/socket/socket-lin.go @@ -15,7 +15,7 @@ import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) -const socketPath = "/tmp/ssui-api.sock" +const socketPath = "/var/run/ssui.sock" func StartSocketServer(wg *sync.WaitGroup) { logger.Socket.Info("Starting Unix socket server...") diff --git a/src/api/socket/socket-win.go b/src/api/socket/socket-win.go index 3598b772..dd840eca 100644 --- a/src/api/socket/socket-win.go +++ b/src/api/socket/socket-win.go @@ -14,7 +14,7 @@ import ( "github.com/microsoft/go-winio" ) -const pipePath = `\\.\pipe\ssui-api` +const pipePath = `\\.\pipe\ssui` func StartSocketServer(wg *sync.WaitGroup) { logger.Socket.Info("Starting named pipe server...") From fc3117e5e13876f6b47ba0e028a97e8650bf379b Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 01:34:40 +0200 Subject: [PATCH 33/93] updated devcontainer config --- .devcontainer/devcontainer.json | 40 ++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0ed4e03b..0346bdee 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,10 @@ { - "name": "StationeersServerUI", + "name": "SteamSererUI", "build": { "dockerfile": "Dockerfile", - "args": { "TZ": "${localEnv:TZ:Europe/Stockholm}" } + "args": { + "TZ": "${localEnv:TZ:Europe/Stockholm}" + } }, "workspaceFolder": "/workspaces/project", "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/project,type=bind,consistency=cached", @@ -11,11 +13,17 @@ "version": "1.25.0" } }, - "forwardPorts": [8443], - "runArgs": ["-p=27015:27015/udp", "-p=27016:27016/udp"], + "forwardPorts": [ + 8443 + ], + "runArgs": [ + "-p=27015:27015/udp", + "-p=27016:27016/udp" + ], "portsAttributes": { - "8443": { "label": "Go Backend Server" } - + "8443": { + "label": "Go Backend Server" + } }, "customizations": { "vscode": { @@ -23,11 +31,27 @@ "golang.go", "svelte.svelte-vscode", "supermaven.supermaven", - "eamodio.gitlens" + "eamodio.gitlens", + "pkief.material-icon-theme" ], "settings": { "go.toolsManagement.autoUpdate": true, - "svelte.enable-ts-plugin": true + "svelte.enable-ts-plugin": true, + "workbench.iconTheme": "material-icon-theme", + "workbench.colorTheme": "Solarized Dark", + "material-icon-theme.files.customClones": [ + { + "name": "ssui", + "base": "rocket", + "color": "blue-500", + "fileExtensions": [ + "ssui" + ] + } + ], + "files.associations": { + "*.ssui": "json" + } } } }, From fed75087d3c9f72cacb691d8db1e3d415a34af6e Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 01:40:18 +0200 Subject: [PATCH 34/93] moved discordbot and discordrpc to dedicated discord folder --- src/core/loader/afterstart.go | 2 +- src/core/loader/loader.go | 2 +- src/{ => discord}/discordbot/connectedplayers.go | 0 src/{ => discord}/discordbot/controlpanel.go | 0 src/{ => discord}/discordbot/discordgo-license.md | 0 src/{ => discord}/discordbot/embeds.go | 0 src/{ => discord}/discordbot/handleBlacklist.go | 0 src/{ => discord}/discordbot/handleReactions.go | 0 src/{ => discord}/discordbot/handleSlashcommands.go | 0 src/{ => discord}/discordbot/interface.go | 0 src/{ => discord}/discordbot/logstream.go | 0 src/{ => discord}/discordbot/registerSlashcommands.go | 0 src/{ => discord}/discordbot/sendMessage.go | 0 src/{ => discord}/discordbot/types.go | 0 src/{ => discord}/discordrpc/discordrpc.go | 0 src/managers/detectionmgr/detector.go | 2 +- src/managers/detectionmgr/handlers.go | 2 +- src/managers/detectionmgr/logstream.go | 2 +- 18 files changed, 5 insertions(+), 5 deletions(-) rename src/{ => discord}/discordbot/connectedplayers.go (100%) rename src/{ => discord}/discordbot/controlpanel.go (100%) rename src/{ => discord}/discordbot/discordgo-license.md (100%) rename src/{ => discord}/discordbot/embeds.go (100%) rename src/{ => discord}/discordbot/handleBlacklist.go (100%) rename src/{ => discord}/discordbot/handleReactions.go (100%) rename src/{ => discord}/discordbot/handleSlashcommands.go (100%) rename src/{ => discord}/discordbot/interface.go (100%) rename src/{ => discord}/discordbot/logstream.go (100%) rename src/{ => discord}/discordbot/registerSlashcommands.go (100%) rename src/{ => discord}/discordbot/sendMessage.go (100%) rename src/{ => discord}/discordbot/types.go (100%) rename src/{ => discord}/discordrpc/discordrpc.go (100%) diff --git a/src/core/loader/afterstart.go b/src/core/loader/afterstart.go index 91c3824b..4783164e 100644 --- a/src/core/loader/afterstart.go +++ b/src/core/loader/afterstart.go @@ -4,7 +4,7 @@ import ( "sync" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordrpc" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordrpc" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index b042b6a4..d02f1a27 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -8,7 +8,7 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" diff --git a/src/discordbot/connectedplayers.go b/src/discord/discordbot/connectedplayers.go similarity index 100% rename from src/discordbot/connectedplayers.go rename to src/discord/discordbot/connectedplayers.go diff --git a/src/discordbot/controlpanel.go b/src/discord/discordbot/controlpanel.go similarity index 100% rename from src/discordbot/controlpanel.go rename to src/discord/discordbot/controlpanel.go diff --git a/src/discordbot/discordgo-license.md b/src/discord/discordbot/discordgo-license.md similarity index 100% rename from src/discordbot/discordgo-license.md rename to src/discord/discordbot/discordgo-license.md diff --git a/src/discordbot/embeds.go b/src/discord/discordbot/embeds.go similarity index 100% rename from src/discordbot/embeds.go rename to src/discord/discordbot/embeds.go diff --git a/src/discordbot/handleBlacklist.go b/src/discord/discordbot/handleBlacklist.go similarity index 100% rename from src/discordbot/handleBlacklist.go rename to src/discord/discordbot/handleBlacklist.go diff --git a/src/discordbot/handleReactions.go b/src/discord/discordbot/handleReactions.go similarity index 100% rename from src/discordbot/handleReactions.go rename to src/discord/discordbot/handleReactions.go diff --git a/src/discordbot/handleSlashcommands.go b/src/discord/discordbot/handleSlashcommands.go similarity index 100% rename from src/discordbot/handleSlashcommands.go rename to src/discord/discordbot/handleSlashcommands.go diff --git a/src/discordbot/interface.go b/src/discord/discordbot/interface.go similarity index 100% rename from src/discordbot/interface.go rename to src/discord/discordbot/interface.go diff --git a/src/discordbot/logstream.go b/src/discord/discordbot/logstream.go similarity index 100% rename from src/discordbot/logstream.go rename to src/discord/discordbot/logstream.go diff --git a/src/discordbot/registerSlashcommands.go b/src/discord/discordbot/registerSlashcommands.go similarity index 100% rename from src/discordbot/registerSlashcommands.go rename to src/discord/discordbot/registerSlashcommands.go diff --git a/src/discordbot/sendMessage.go b/src/discord/discordbot/sendMessage.go similarity index 100% rename from src/discordbot/sendMessage.go rename to src/discord/discordbot/sendMessage.go diff --git a/src/discordbot/types.go b/src/discord/discordbot/types.go similarity index 100% rename from src/discordbot/types.go rename to src/discord/discordbot/types.go diff --git a/src/discordrpc/discordrpc.go b/src/discord/discordrpc/discordrpc.go similarity index 100% rename from src/discordrpc/discordrpc.go rename to src/discord/discordrpc/discordrpc.go diff --git a/src/managers/detectionmgr/detector.go b/src/managers/detectionmgr/detector.go index 24e8c6df..743f915a 100644 --- a/src/managers/detectionmgr/detector.go +++ b/src/managers/detectionmgr/detector.go @@ -8,7 +8,7 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" ) /* diff --git a/src/managers/detectionmgr/handlers.go b/src/managers/detectionmgr/handlers.go index d8ff75f3..3b09eb6b 100644 --- a/src/managers/detectionmgr/handlers.go +++ b/src/managers/detectionmgr/handlers.go @@ -7,7 +7,7 @@ import ( "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) diff --git a/src/managers/detectionmgr/logstream.go b/src/managers/detectionmgr/logstream.go index 264ec04e..7014f8c2 100644 --- a/src/managers/detectionmgr/logstream.go +++ b/src/managers/detectionmgr/logstream.go @@ -4,7 +4,7 @@ package detectionmgr import ( "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discordbot" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" ) From 010ff171f4e8e957b1e6edd62299d828ab704b9b Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 02:29:55 +0200 Subject: [PATCH 35/93] apply fix for SSE Stream race condition in v7 SSE handling Fixes #120 originally fixed in #117 --- src/core/ssestream/ssemanager.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core/ssestream/ssemanager.go b/src/core/ssestream/ssemanager.go index 16e86919..48c56e86 100644 --- a/src/core/ssestream/ssemanager.go +++ b/src/core/ssestream/ssemanager.go @@ -106,10 +106,7 @@ func (m *SSEManager) CreateStreamHandler(streamType string) http.HandlerFunc { notify := r.Context().Done() // Start streaming messages - go m.streamMessages(w, flusher, client, streamType, notify) - - // Wait for client disconnection - <-notify + m.streamMessages(w, flusher, client, notify) } } @@ -118,7 +115,6 @@ func (m *SSEManager) streamMessages( w http.ResponseWriter, flusher http.Flusher, client *Client, - streamType string, notify <-chan struct{}, ) { defer m.removeClient(client) From cd00e8b030674a12a0d048c586792a293e3827f0 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:09:48 +0200 Subject: [PATCH 36/93] change module name from JacksonTheMaster/StationeersServerUI to SteamServerUI/SteamServerUI/v7 --- build/build.go | 2 +- build/version.go | 2 +- go.mod | 2 +- server.go | 12 ++++++------ src/api/TwoBoxForm.go | 6 +++--- src/api/commands.go | 4 ++-- src/api/configpage.go | 6 +++--- src/api/connectedplayers.go | 2 +- src/api/detectionmanagerpage.go | 2 +- src/api/http-sse.go | 2 +- src/api/http.go | 14 +++++++------- src/api/indexpage.go | 6 +++--- src/api/login.go | 10 +++++----- src/api/routes.go | 10 +++++----- src/api/runfile.go | 6 +++--- src/api/runfilegallery.go | 4 ++-- src/api/socket/socket-lin.go | 4 ++-- src/api/socket/socket-win.go | 4 ++-- src/api/start.go | 6 +++--- src/api/svelteui.go | 6 +++--- src/api/systeminfo.go | 4 ++-- src/cli/runtimecommands.go | 14 +++++++------- src/config/configchanger/changeconfig.go | 2 +- src/config/configchanger/saveconfig.go | 6 +++--- src/core/loader/afterstart.go | 10 +++++----- src/core/loader/cmdargs.go | 6 +++--- src/core/loader/helpers.go | 4 ++-- src/core/loader/loader.go | 18 +++++++++--------- src/core/loader/runfile.go | 10 +++++----- src/core/loader/sanitycheck.go | 2 +- src/core/loader/terminalmsg.go | 4 ++-- src/core/security/auth.go | 2 +- src/core/security/tls.go | 4 ++-- src/core/ssestream/ssemanager.go | 2 +- src/core/ssestream/sseutils.go | 2 +- src/discord/discordbot/connectedplayers.go | 4 ++-- src/discord/discordbot/controlpanel.go | 8 ++++---- src/discord/discordbot/handleBlacklist.go | 2 +- src/discord/discordbot/handleReactions.go | 6 +++--- src/discord/discordbot/handleSlashcommands.go | 12 ++++++------ src/discord/discordbot/interface.go | 4 ++-- src/discord/discordbot/logstream.go | 4 ++-- .../discordbot/registerSlashcommands.go | 2 +- src/discord/discordbot/sendMessage.go | 4 ++-- src/discord/discordrpc/discordrpc.go | 2 +- src/localization/localization.go | 4 ++-- src/logger/log-helpers.go | 2 +- src/logger/logger.go | 4 ++-- src/managers/backupmgr/backuphttp.go | 4 ++-- src/managers/backupmgr/backupinterface.go | 4 ++-- src/managers/backupmgr/cleanup.go | 2 +- src/managers/backupmgr/manager.go | 6 +++--- src/managers/backupmgr/restore.go | 2 +- src/managers/backupmgr/watcher.go | 2 +- src/managers/commandmgr/commandmgr.go | 2 +- src/managers/detectionmgr/customdetections.go | 2 +- src/managers/detectionmgr/detector.go | 4 ++-- src/managers/detectionmgr/handlers.go | 6 +++--- src/managers/detectionmgr/logstream.go | 8 ++++---- src/managers/gamemgr/autorestart.go | 6 +++--- src/managers/gamemgr/bepinex.go | 6 +++--- src/managers/gamemgr/processmanagement.go | 6 +++--- src/managers/gamemgr/runcheck.go | 2 +- src/managers/gamemgr/serverlog.go | 4 ++-- src/managers/gamemgr/uuid.go | 2 +- src/setup/autostartscripts.go | 2 +- src/setup/bepinex.go | 6 +++--- src/setup/cleanup.go | 4 ++-- src/setup/install.go | 6 +++--- src/setup/update/updater.go | 4 ++-- src/steamcmd/getappinfo.go | 10 +++++----- src/steamcmd/install.go | 2 +- src/steamcmd/steamcmd-helper.go | 2 +- src/steamcmd/steamcmd.go | 8 ++++---- src/steamserverui/gallery/gallery.go | 4 ++-- src/steamserverui/runfile/argexample.go | 2 +- src/steamserverui/runfile/args.go | 4 ++-- src/steamserverui/runfile/getters.go | 2 +- src/steamserverui/settings/retrieve.go | 2 +- src/steamserverui/settings/save.go | 2 +- 80 files changed, 193 insertions(+), 193 deletions(-) diff --git a/build/build.go b/build/build.go index 5ac9233d..594fd57a 100644 --- a/build/build.go +++ b/build/build.go @@ -15,7 +15,7 @@ import ( "strconv" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) const ( diff --git a/build/version.go b/build/version.go index dc240999..ac30672e 100644 --- a/build/version.go +++ b/build/version.go @@ -13,7 +13,7 @@ import ( "path/filepath" "regexp" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) func main() { diff --git a/go.mod b/go.mod index 66419a87..b1d91ee0 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/JacksonTheMaster/StationeersServerUI/v5 +module github.com/SteamServerUI/SteamServerUI/v7 go 1.25.0 diff --git a/server.go b/server.go index 4e04bd55..c6f72e8b 100644 --- a/server.go +++ b/server.go @@ -24,12 +24,12 @@ import ( "embed" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api/socket" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/cli" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/SteamServerUI/SteamServerUI/v7/src/api" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/socket" + "github.com/SteamServerUI/SteamServerUI/v7/src/cli" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/setup" ) //go:embed UIMod/onboard_bundled diff --git a/src/api/TwoBoxForm.go b/src/api/TwoBoxForm.go index c9bf9318..7ef16e2b 100644 --- a/src/api/TwoBoxForm.go +++ b/src/api/TwoBoxForm.go @@ -5,9 +5,9 @@ import ( "net/http" "text/template" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { diff --git a/src/api/commands.go b/src/api/commands.go index 9b770265..99249861 100644 --- a/src/api/commands.go +++ b/src/api/commands.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" ) type CommandRequest struct { diff --git a/src/api/configpage.go b/src/api/configpage.go index d52bc8f5..870c6cbe 100644 --- a/src/api/configpage.go +++ b/src/api/configpage.go @@ -6,9 +6,9 @@ import ( "net/http" "text/template" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func ServeConfigPage(w http.ResponseWriter, r *http.Request) { diff --git a/src/api/connectedplayers.go b/src/api/connectedplayers.go index ccb61165..bcb69228 100644 --- a/src/api/connectedplayers.go +++ b/src/api/connectedplayers.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/detectionmgr" ) // PrintConnectedPlayersHandler handles HTTP requests to list connected players. diff --git a/src/api/detectionmanagerpage.go b/src/api/detectionmanagerpage.go index 96d5f360..19ff5a5b 100644 --- a/src/api/detectionmanagerpage.go +++ b/src/api/detectionmanagerpage.go @@ -6,7 +6,7 @@ import ( "io/fs" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) func ServeDetectionManager(w http.ResponseWriter, r *http.Request) { diff --git a/src/api/http-sse.go b/src/api/http-sse.go index 447704db..4d0010b3 100644 --- a/src/api/http-sse.go +++ b/src/api/http-sse.go @@ -3,7 +3,7 @@ package api import ( "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/ssestream" ) // handler for the /console endpoint diff --git a/src/api/http.go b/src/api/http.go index 479ef546..cae38d09 100644 --- a/src/api/http.go +++ b/src/api/http.go @@ -8,13 +8,13 @@ import ( "os" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/detectionmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" ) // StartServer HTTP handler diff --git a/src/api/indexpage.go b/src/api/indexpage.go index f42c732a..db6adcdf 100644 --- a/src/api/indexpage.go +++ b/src/api/indexpage.go @@ -5,9 +5,9 @@ import ( "net/http" "text/template" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func ServeIndex(w http.ResponseWriter, r *http.Request) { diff --git a/src/api/login.go b/src/api/login.go index 90b853f0..1423fb6a 100644 --- a/src/api/login.go +++ b/src/api/login.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config/configchanger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) var setupReminderCount = 0 // to limit the number of setup reminders shown to the user diff --git a/src/api/routes.go b/src/api/routes.go index f1f50c91..d6ae6d11 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -4,11 +4,11 @@ import ( "io/fs" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config/configchanger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/settings" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/backupmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/detectionmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/settings" ) func SetupRoutes() (*http.ServeMux, *http.ServeMux) { diff --git a/src/api/runfile.go b/src/api/runfile.go index 2b07a538..5dfdc488 100644 --- a/src/api/runfile.go +++ b/src/api/runfile.go @@ -6,9 +6,9 @@ import ( "net/http" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" ) // APIGameArg is a DTO for GameArg, including RuntimeValue and all fields diff --git a/src/api/runfilegallery.go b/src/api/runfilegallery.go index eff60b5d..6145185d 100644 --- a/src/api/runfilegallery.go +++ b/src/api/runfilegallery.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/gallery" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/gallery" ) // response wraps API responses diff --git a/src/api/socket/socket-lin.go b/src/api/socket/socket-lin.go index 93a0dd80..ea1cdb61 100644 --- a/src/api/socket/socket-lin.go +++ b/src/api/socket/socket-lin.go @@ -11,8 +11,8 @@ import ( "os" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/api" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) const socketPath = "/var/run/ssui.sock" diff --git a/src/api/socket/socket-win.go b/src/api/socket/socket-win.go index dd840eca..ceed2717 100644 --- a/src/api/socket/socket-win.go +++ b/src/api/socket/socket-win.go @@ -9,8 +9,8 @@ import ( "net/http" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/api" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/api" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/microsoft/go-winio" ) diff --git a/src/api/start.go b/src/api/start.go index 542cff82..5c4b55e2 100644 --- a/src/api/start.go +++ b/src/api/start.go @@ -7,9 +7,9 @@ import ( "net/http/pprof" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) type APIServerLogger struct{} diff --git a/src/api/svelteui.go b/src/api/svelteui.go index 548f16dc..2475dc95 100644 --- a/src/api/svelteui.go +++ b/src/api/svelteui.go @@ -7,9 +7,9 @@ import ( "net/http" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) var reloadMu sync.Mutex diff --git a/src/api/systeminfo.go b/src/api/systeminfo.go index d378c195..a3386555 100644 --- a/src/api/systeminfo.go +++ b/src/api/systeminfo.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/systeminfo" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/systeminfo" ) func HandleGetOsStats(w http.ResponseWriter, r *http.Request) { diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 673a76e0..8c617127 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -18,13 +18,13 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" ) // ANSI escape codes for green text and reset diff --git a/src/config/configchanger/changeconfig.go b/src/config/configchanger/changeconfig.go index 25a89aeb..073d7b40 100644 --- a/src/config/configchanger/changeconfig.go +++ b/src/config/configchanger/changeconfig.go @@ -8,7 +8,7 @@ import ( "reflect" "strconv" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) func SaveConfigForm(w http.ResponseWriter, r *http.Request) { diff --git a/src/config/configchanger/saveconfig.go b/src/config/configchanger/saveconfig.go index 6163b3fb..2378a3b8 100644 --- a/src/config/configchanger/saveconfig.go +++ b/src/config/configchanger/saveconfig.go @@ -1,9 +1,9 @@ package configchanger import ( - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/loader" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func SaveConfig(cfg *config.JsonConfig, reloadBackend ...bool) error { diff --git a/src/core/loader/afterstart.go b/src/core/loader/afterstart.go index 4783164e..b5dc2d51 100644 --- a/src/core/loader/afterstart.go +++ b/src/core/loader/afterstart.go @@ -3,11 +3,11 @@ package loader import ( "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordrpc" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/discord/discordrpc" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/setup" ) func AfterStartComplete(wg *sync.WaitGroup) { diff --git a/src/core/loader/cmdargs.go b/src/core/loader/cmdargs.go index 1b04418c..76d2c2ea 100644 --- a/src/core/loader/cmdargs.go +++ b/src/core/loader/cmdargs.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/security" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // Define flags matching the config variable names diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index fccb5e1b..540489c7 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -7,8 +7,8 @@ import ( "strings" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func PrintConfigDetails(logLevel ...string) { diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index d02f1a27..4d1c15fe 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -7,15 +7,15 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/detectionmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/discord/discordbot" + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/backupmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/detectionmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/setup" + "github.com/SteamServerUI/SteamServerUI/v7/src/setup/update" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" ) // only call this once at startup diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index 5af0c2d2..bf16600e 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -4,11 +4,11 @@ import ( "fmt" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" ) // used via Runfile Gallery diff --git a/src/core/loader/sanitycheck.go b/src/core/loader/sanitycheck.go index e7201e8f..fd5ff78a 100644 --- a/src/core/loader/sanitycheck.go +++ b/src/core/loader/sanitycheck.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) var containerCheckWG = sync.WaitGroup{} diff --git a/src/core/loader/terminalmsg.go b/src/core/loader/terminalmsg.go index 588eed6f..fe1eb952 100644 --- a/src/core/loader/terminalmsg.go +++ b/src/core/loader/terminalmsg.go @@ -4,8 +4,8 @@ import ( "runtime" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // PrintStartupMessage prints a stylish startup message to the terminal diff --git a/src/core/security/auth.go b/src/core/security/auth.go index a052eb24..0194fbef 100644 --- a/src/core/security/auth.go +++ b/src/core/security/auth.go @@ -6,7 +6,7 @@ package security import ( "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" diff --git a/src/core/security/tls.go b/src/core/security/tls.go index 2d4a9756..e478ef1e 100644 --- a/src/core/security/tls.go +++ b/src/core/security/tls.go @@ -13,8 +13,8 @@ import ( "path/filepath" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // EnsureTLSCerts ensures TLS certificates exist and are valid at config.GetTLSCertPath() and config.GetTLSKeyPath(), generating self-signed ones if needed. diff --git a/src/core/ssestream/ssemanager.go b/src/core/ssestream/ssemanager.go index 48c56e86..10b0eb51 100644 --- a/src/core/ssestream/ssemanager.go +++ b/src/core/ssestream/ssemanager.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) // The SSE blocking issue is NOT related to the backend; the API handles 200 clients per channel fine. diff --git a/src/core/ssestream/sseutils.go b/src/core/ssestream/sseutils.go index eafa4a51..d7c98a07 100644 --- a/src/core/ssestream/sseutils.go +++ b/src/core/ssestream/sseutils.go @@ -2,7 +2,7 @@ package ssestream import ( - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) // Global managers for SSE streams diff --git a/src/discord/discordbot/connectedplayers.go b/src/discord/discordbot/connectedplayers.go index 6817a560..dd6d7dfe 100644 --- a/src/discord/discordbot/connectedplayers.go +++ b/src/discord/discordbot/connectedplayers.go @@ -6,8 +6,8 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) var ( diff --git a/src/discord/discordbot/controlpanel.go b/src/discord/discordbot/controlpanel.go index 323e472d..1cb31f5c 100644 --- a/src/discord/discordbot/controlpanel.go +++ b/src/discord/discordbot/controlpanel.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" "github.com/bwmarrin/discordgo" ) diff --git a/src/discord/discordbot/handleBlacklist.go b/src/discord/discordbot/handleBlacklist.go index 9af21503..164b23e1 100644 --- a/src/discord/discordbot/handleBlacklist.go +++ b/src/discord/discordbot/handleBlacklist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) var blacklistMutex sync.Mutex diff --git a/src/discord/discordbot/handleReactions.go b/src/discord/discordbot/handleReactions.go index 351c9d2b..fb7c523b 100644 --- a/src/discord/discordbot/handleReactions.go +++ b/src/discord/discordbot/handleReactions.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" "github.com/bwmarrin/discordgo" ) diff --git a/src/discord/discordbot/handleSlashcommands.go b/src/discord/discordbot/handleSlashcommands.go index 20d315aa..4c7e1e4c 100644 --- a/src/discord/discordbot/handleSlashcommands.go +++ b/src/discord/discordbot/handleSlashcommands.go @@ -7,12 +7,12 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/backupmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/backupmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" "github.com/bwmarrin/discordgo" ) diff --git a/src/discord/discordbot/interface.go b/src/discord/discordbot/interface.go index 773f8f51..057f499e 100644 --- a/src/discord/discordbot/interface.go +++ b/src/discord/discordbot/interface.go @@ -3,8 +3,8 @@ package discordbot import ( "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/bwmarrin/discordgo" ) diff --git a/src/discord/discordbot/logstream.go b/src/discord/discordbot/logstream.go index 9212d9a5..672a0446 100644 --- a/src/discord/discordbot/logstream.go +++ b/src/discord/discordbot/logstream.go @@ -1,8 +1,8 @@ package discordbot import ( - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // PassLogMessageToDiscordLogBuffer is called from the detection module to add a log message to the buffer. diff --git a/src/discord/discordbot/registerSlashcommands.go b/src/discord/discordbot/registerSlashcommands.go index 8756d3ce..00afe0a2 100644 --- a/src/discord/discordbot/registerSlashcommands.go +++ b/src/discord/discordbot/registerSlashcommands.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/bwmarrin/discordgo" ) diff --git a/src/discord/discordbot/sendMessage.go b/src/discord/discordbot/sendMessage.go index 1654183f..6f06c52e 100644 --- a/src/discord/discordbot/sendMessage.go +++ b/src/discord/discordbot/sendMessage.go @@ -4,9 +4,9 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/bwmarrin/discordgo" ) diff --git a/src/discord/discordrpc/discordrpc.go b/src/discord/discordrpc/discordrpc.go index e8bb22ec..69131ef7 100644 --- a/src/discord/discordrpc/discordrpc.go +++ b/src/discord/discordrpc/discordrpc.go @@ -3,7 +3,7 @@ package discordrpc import ( "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/jacksonthemaster/discordrichpresence" ) diff --git a/src/localization/localization.go b/src/localization/localization.go index 55e77c0e..7a0e417d 100644 --- a/src/localization/localization.go +++ b/src/localization/localization.go @@ -6,8 +6,8 @@ import ( "strings" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // translations stores language code to key-value pairs diff --git a/src/logger/log-helpers.go b/src/logger/log-helpers.go index 76ad7b69..6c99e8dd 100644 --- a/src/logger/log-helpers.go +++ b/src/logger/log-helpers.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) func (l *Logger) writeToFile(logLine, subsystem string) { diff --git a/src/logger/logger.go b/src/logger/logger.go index e6a155d8..1732d345 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/ssestream" ) // Logger instances diff --git a/src/managers/backupmgr/backuphttp.go b/src/managers/backupmgr/backuphttp.go index 729cc1df..c93419b4 100644 --- a/src/managers/backupmgr/backuphttp.go +++ b/src/managers/backupmgr/backuphttp.go @@ -7,8 +7,8 @@ import ( "strconv" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" ) // HTTPHandler provides HTTP endpoints for backup operations diff --git a/src/managers/backupmgr/backupinterface.go b/src/managers/backupmgr/backupinterface.go index 6f0964be..1c3d55f0 100644 --- a/src/managers/backupmgr/backupinterface.go +++ b/src/managers/backupmgr/backupinterface.go @@ -4,8 +4,8 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/google/uuid" ) diff --git a/src/managers/backupmgr/cleanup.go b/src/managers/backupmgr/cleanup.go index 946b1b84..4621691b 100644 --- a/src/managers/backupmgr/cleanup.go +++ b/src/managers/backupmgr/cleanup.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // Cleanup performs backup cleanup according to retention policy diff --git a/src/managers/backupmgr/manager.go b/src/managers/backupmgr/manager.go index 69338718..5d61ce12 100644 --- a/src/managers/backupmgr/manager.go +++ b/src/managers/backupmgr/manager.go @@ -8,9 +8,9 @@ import ( "sort" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" "github.com/fsnotify/fsnotify" ) diff --git a/src/managers/backupmgr/restore.go b/src/managers/backupmgr/restore.go index 8ce94533..afcfc283 100644 --- a/src/managers/backupmgr/restore.go +++ b/src/managers/backupmgr/restore.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // RestoreBackup restores a backup with the given index diff --git a/src/managers/backupmgr/watcher.go b/src/managers/backupmgr/watcher.go index 179126d4..05a2a966 100644 --- a/src/managers/backupmgr/watcher.go +++ b/src/managers/backupmgr/watcher.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/fsnotify/fsnotify" ) diff --git a/src/managers/commandmgr/commandmgr.go b/src/managers/commandmgr/commandmgr.go index 30b55d5e..0b0c69b7 100644 --- a/src/managers/commandmgr/commandmgr.go +++ b/src/managers/commandmgr/commandmgr.go @@ -7,7 +7,7 @@ import ( "path/filepath" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) var mutex = &sync.Mutex{} diff --git a/src/managers/detectionmgr/customdetections.go b/src/managers/detectionmgr/customdetections.go index 459bff75..cf656485 100644 --- a/src/managers/detectionmgr/customdetections.go +++ b/src/managers/detectionmgr/customdetections.go @@ -10,7 +10,7 @@ import ( "regexp" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) /* diff --git a/src/managers/detectionmgr/detector.go b/src/managers/detectionmgr/detector.go index 743f915a..65be1fb2 100644 --- a/src/managers/detectionmgr/detector.go +++ b/src/managers/detectionmgr/detector.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/discord/discordbot" ) /* diff --git a/src/managers/detectionmgr/handlers.go b/src/managers/detectionmgr/handlers.go index 3b09eb6b..9dc421cf 100644 --- a/src/managers/detectionmgr/handlers.go +++ b/src/managers/detectionmgr/handlers.go @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/ssestream" + "github.com/SteamServerUI/SteamServerUI/v7/src/discord/discordbot" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) /* diff --git a/src/managers/detectionmgr/logstream.go b/src/managers/detectionmgr/logstream.go index 7014f8c2..d54fe498 100644 --- a/src/managers/detectionmgr/logstream.go +++ b/src/managers/detectionmgr/logstream.go @@ -2,10 +2,10 @@ package detectionmgr import ( - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/discord/discordbot" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/ssestream" + "github.com/SteamServerUI/SteamServerUI/v7/src/discord/discordbot" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) /* diff --git a/src/managers/gamemgr/autorestart.go b/src/managers/gamemgr/autorestart.go index 3bec7653..e4a8aede 100644 --- a/src/managers/gamemgr/autorestart.go +++ b/src/managers/gamemgr/autorestart.go @@ -4,9 +4,9 @@ import ( "strconv" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" ) var ( diff --git a/src/managers/gamemgr/bepinex.go b/src/managers/gamemgr/bepinex.go index 7a66aa4b..90383b6d 100644 --- a/src/managers/gamemgr/bepinex.go +++ b/src/managers/gamemgr/bepinex.go @@ -5,9 +5,9 @@ import ( "os" "path/filepath" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" ) // BepInEx version: 5.4.23.2 or v5-lts diff --git a/src/managers/gamemgr/processmanagement.go b/src/managers/gamemgr/processmanagement.go index 6217fd80..2b6c4255 100644 --- a/src/managers/gamemgr/processmanagement.go +++ b/src/managers/gamemgr/processmanagement.go @@ -12,9 +12,9 @@ import ( "syscall" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" ) var ( diff --git a/src/managers/gamemgr/runcheck.go b/src/managers/gamemgr/runcheck.go index a57734f4..605eeeea 100644 --- a/src/managers/gamemgr/runcheck.go +++ b/src/managers/gamemgr/runcheck.go @@ -4,7 +4,7 @@ import ( "runtime" "syscall" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // InternalIsServerRunning checks if the server process is running. diff --git a/src/managers/gamemgr/serverlog.go b/src/managers/gamemgr/serverlog.go index fc950cff..504ad287 100644 --- a/src/managers/gamemgr/serverlog.go +++ b/src/managers/gamemgr/serverlog.go @@ -10,8 +10,8 @@ import ( "strconv" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/core/ssestream" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/ssestream" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // readPipe for Windows diff --git a/src/managers/gamemgr/uuid.go b/src/managers/gamemgr/uuid.go index e968648b..f3ffde77 100644 --- a/src/managers/gamemgr/uuid.go +++ b/src/managers/gamemgr/uuid.go @@ -1,7 +1,7 @@ package gamemgr import ( - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/google/uuid" ) diff --git a/src/setup/autostartscripts.go b/src/setup/autostartscripts.go index 4fe653d4..ae29edcd 100644 --- a/src/setup/autostartscripts.go +++ b/src/setup/autostartscripts.go @@ -6,7 +6,7 @@ import ( "os" "runtime" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) func SetupAutostartScripts() { diff --git a/src/setup/bepinex.go b/src/setup/bepinex.go index ce2d185e..89937ccc 100644 --- a/src/setup/bepinex.go +++ b/src/setup/bepinex.go @@ -6,9 +6,9 @@ import ( "runtime" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamcmd" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" ) // BepInEx version: 5.4.23.2 or v5-lts diff --git a/src/setup/cleanup.go b/src/setup/cleanup.go index 323be66d..6ef81c57 100644 --- a/src/setup/cleanup.go +++ b/src/setup/cleanup.go @@ -7,8 +7,8 @@ import ( "regexp" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func CleanUpOldUIModFolderFiles() error { diff --git a/src/setup/install.go b/src/setup/install.go index 5c25cd95..6cd8fb94 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -14,9 +14,9 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/setup/update" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/setup/update" ) var downloadBranch string // Holds the branch to download from diff --git a/src/setup/update/updater.go b/src/setup/update/updater.go index f5ef8b7c..3e37ed8d 100644 --- a/src/setup/update/updater.go +++ b/src/setup/update/updater.go @@ -14,8 +14,8 @@ import ( "syscall" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // githubRelease represents the structure of a GitHub release response diff --git a/src/steamcmd/getappinfo.go b/src/steamcmd/getappinfo.go index ab47bda6..5a8bd0a4 100644 --- a/src/steamcmd/getappinfo.go +++ b/src/steamcmd/getappinfo.go @@ -13,11 +13,11 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/commandmgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" ) var ( diff --git a/src/steamcmd/install.go b/src/steamcmd/install.go index 7be15268..61536be1 100644 --- a/src/steamcmd/install.go +++ b/src/steamcmd/install.go @@ -3,7 +3,7 @@ package steamcmd import ( "os" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) func installSteamCMD(platform string, steamCMDDir string, downloadURL string, extractFunc ExtractorFunc) (int, error) { diff --git a/src/steamcmd/steamcmd-helper.go b/src/steamcmd/steamcmd-helper.go index 4caee4fe..159be7ef 100644 --- a/src/steamcmd/steamcmd-helper.go +++ b/src/steamcmd/steamcmd-helper.go @@ -17,7 +17,7 @@ import ( "strings" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // isRelSymlink ensures `link` resolves to a path within `root`. diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index ce6c4aa1..be56777e 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -10,11 +10,11 @@ import ( "strings" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/managers/gamemgr" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/steamserverui/runfile" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) var steamMu sync.Mutex diff --git a/src/steamserverui/gallery/gallery.go b/src/steamserverui/gallery/gallery.go index 00db95ec..2ddac3bc 100644 --- a/src/steamserverui/gallery/gallery.go +++ b/src/steamserverui/gallery/gallery.go @@ -10,8 +10,8 @@ import ( "strings" "sync" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // GalleryRunfile represents a runfile in the gallery diff --git a/src/steamserverui/runfile/argexample.go b/src/steamserverui/runfile/argexample.go index ad248216..7d84a6d8 100644 --- a/src/steamserverui/runfile/argexample.go +++ b/src/steamserverui/runfile/argexample.go @@ -3,7 +3,7 @@ package runfile import ( "fmt" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) // unused diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index 9bda8eec..d3c34898 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -12,8 +12,8 @@ import ( "sync" "time" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // Package-level mutex for file operations diff --git a/src/steamserverui/runfile/getters.go b/src/steamserverui/runfile/getters.go index b6770787..580ef9e2 100644 --- a/src/steamserverui/runfile/getters.go +++ b/src/steamserverui/runfile/getters.go @@ -5,7 +5,7 @@ import ( "runtime" "strings" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) // GetAllArgs returns all GameArgs from the runfile diff --git a/src/steamserverui/settings/retrieve.go b/src/steamserverui/settings/retrieve.go index 8cf82c71..da418d99 100644 --- a/src/steamserverui/settings/retrieve.go +++ b/src/steamserverui/settings/retrieve.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) // package settings handles API communication with the config values in package config via getter /setter functions. diff --git a/src/steamserverui/settings/save.go b/src/steamserverui/settings/save.go index ca857c27..6b789ad1 100644 --- a/src/steamserverui/settings/save.go +++ b/src/steamserverui/settings/save.go @@ -6,7 +6,7 @@ import ( "io" "net/http" - "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config" ) // package settings handles API communication with the config values in package config via getter /setter functions. From 0383a7546fd06a3e6d4cdd0cf9f02d4dcd726636 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:13:48 +0200 Subject: [PATCH 37/93] adjust executable name to SteamServerUI --- build/build.go | 4 ++-- src/setup/update/updater.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/build.go b/build/build.go index 594fd57a..6848b80c 100644 --- a/build/build.go +++ b/build/build.go @@ -62,9 +62,9 @@ func main() { // Prepare the output file name with the new version, branch, and platform var outputName string if config.Branch == "release" { - outputName = fmt.Sprintf("StationeersServerControlv%s", newVersion) + outputName = fmt.Sprintf("SteamServerUIv%s", newVersion) } else { - outputName = fmt.Sprintf("StationeersServerControlv%s_%s", newVersion, config.Branch) + outputName = fmt.Sprintf("SteamServerUIv%s_%s", newVersion, config.Branch) } // Append appropriate extension based on platform diff --git a/src/setup/update/updater.go b/src/setup/update/updater.go index 3e37ed8d..a7440752 100644 --- a/src/setup/update/updater.go +++ b/src/setup/update/updater.go @@ -100,7 +100,7 @@ func UpdateExecutable() error { if runtime.GOOS != "windows" { expectedExt = ".x86_64" } - expectedExe := fmt.Sprintf("StationeersServerControl%s%s", latestRelease.TagName, expectedExt) + expectedExe := fmt.Sprintf("SteamServerUI%s%s", latestRelease.TagName, expectedExt) // Find the asset var downloadURL string From 16a86d5136670d3cd20b81b5290923aa75080b0a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:16:04 +0200 Subject: [PATCH 38/93] bump version to v7 --- frontend/package-lock.json | 9 +++++++-- src/config/config.go | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 934708ee..6d326d98 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "steamserverui", - "version": "v5.5.8", + "version": "v7.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "steamserverui", - "version": "v5.5.8", + "version": "v7.0.0", "license": "proprietary", "dependencies": { "https": "^1.0.0" @@ -882,6 +882,7 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -929,6 +930,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1662,6 +1664,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1839,6 +1842,7 @@ "integrity": "sha512-UY+OhrWK7WI22bCZ00P/M3HtyWgwJPi9IxSRkoAE2MeAy6kl7ZlZWJZ8RaB+X4KD/G+wjis+cGVnVYaoqbzBqg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1899,6 +1903,7 @@ "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/config/config.go b/src/config/config.go index aaba18e9..2033deca 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,8 +11,8 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.7.1" - Branch = "release" + Version = "7.0.0" + Branch = "v7-nightly" ) /* From 70ea29ba909f5a32d480e8b2dabd1b863f0fa8f5 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:19:01 +0200 Subject: [PATCH 39/93] update startup banner --- src/core/loader/terminalmsg.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/loader/terminalmsg.go b/src/core/loader/terminalmsg.go index fe1eb952..1c9370ef 100644 --- a/src/core/loader/terminalmsg.go +++ b/src/core/loader/terminalmsg.go @@ -15,14 +15,14 @@ func printStartupMessage() { logger.Core.Cleanf("") // Main ASCII art logo - logger.Core.Cleanf(" ███████╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗███████╗███████╗██████╗ ███████╗ ███████╗██╗ ██╗██╗") - logger.Core.Cleanf(" ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║██╔════╝██╔════╝██╔══██╗██╔════╝ ██╔════╝██║ ██║██║") - logger.Core.Cleanf(" ███████╗ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██████╔╝███████╗█████╗███████╗██║ ██║██║") - logger.Core.Cleanf(" ╚════██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗╚════██║╚════╝╚════██║██║ ██║██║") - logger.Core.Cleanf(" ███████║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ██║███████║ ███████║╚██████╔╝██║") - logger.Core.Cleanf(" ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚══════╝ ╚═════╝ ╚═╝") + logger.Core.Cleanf(" ███████╗████████╗███████╗ █████╗ ███╗ ███╗███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗") + logger.Core.Cleanf(" ██╔════╝╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗██║ ██║██║") + logger.Core.Cleanf(" ███████╗ ██║ █████╗ ███████║██╔████╔██║███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝██║ ██║██║") + logger.Core.Cleanf(" ╚════██║ ██║ ██╔══╝ ██╔══██║██║╚██╔╝██║╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗██║ ██║██║") + logger.Core.Cleanf(" ███████║ ██║ ███████╗██║ ██║██║ ╚═╝ ██║███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║╚██████╔╝██║") + logger.Core.Cleanf(" ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝") logger.Core.Cleanf(" ╔═══════════════════════════════════════════════════════════════════════════════════════════════════╗") - logger.Core.Cleanf(" ║ 🎮 YOUR ONE-STOP SHOP FOR RUNNING A STATIONEERS SERVER 🎮 ║") + logger.Core.Cleanf(" ║ 🎮 YOUR ONE-STOP SHOP FOR RUNNING A STEAMCMD SERVER 🎮 ║") logger.Core.Cleanf(" ║ 🚀 Version: %s 📅 %s 💻 Runtime: %.3s/%s ║", config.GetVersion(), time.Now().Format("2006-01-02 15:04"), From 6e2d56daaf4d069ae44471424eb9a0662f5dfb0c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:31:07 +0200 Subject: [PATCH 40/93] revert unix socket path to /tmp --- src/api/socket/examples.md | 8 ++++---- src/api/socket/socket-lin.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/socket/examples.md b/src/api/socket/examples.md index f3ee2541..bccf7cbe 100644 --- a/src/api/socket/examples.md +++ b/src/api/socket/examples.md @@ -5,7 +5,7 @@ This guide explains how to interact with the socket-based `SSUI-API`, which expo ## Overview The socket server reuses the HTTP routes defined in `routes.go` but serves them over: -- **Linux**: A Unix socket at `/var/run/ssui.sock`. +- **Linux**: A Unix socket at `/tmp/ssui.sock`. - **Windows**: A named pipe at `\\.\pipe\ssui`. This is similar to how Docker uses `/var/run/docker.sock` for local API access. @@ -27,7 +27,7 @@ This is similar to how Docker uses `/var/run/docker.sock` for local API access. Use `curl` with the `--unix-socket` flag to send HTTP requests to the socket. Example for the `/api/v2/settings` endpoint: ```bash -curl --unix-socket /var/run/ssui.sock http://localhost/api/v2/settings +curl --unix-socket /tmp/ssui.sock http://localhost/api/v2/settings ``` **Expected Output**: JSON response from the `settings.RetrieveSettings` handler, e.g.: @@ -110,14 +110,14 @@ All routes from `routes.go` (e.g., `/api/v2/server/start`, `/api/v2/backups`) ar - **Linux**: ```bash - curl --unix-socket /var/run/ssui.sock http://localhost/api/v2/backups + curl --unix-socket /tmp/ssui.sock http://localhost/api/v2/backups ``` - **Windows**: Edit `$endpoint` in `test_namedpipe.ps1`, e.g., `$endpoint = "/api/v2/backups"`. For POST requests (e.g., `/api/v2/server/start`), add a JSON payload: - **Linux**: ```bash - curl --unix-socket /var/run/ssui.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start + curl --unix-socket /tmp/ssui.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start ``` - **Windows**: Update the PowerShell script’s `$request`: ```powershell diff --git a/src/api/socket/socket-lin.go b/src/api/socket/socket-lin.go index ea1cdb61..76ef5f8d 100644 --- a/src/api/socket/socket-lin.go +++ b/src/api/socket/socket-lin.go @@ -15,7 +15,7 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) -const socketPath = "/var/run/ssui.sock" +const socketPath = "/tmp/ssui.sock" func StartSocketServer(wg *sync.WaitGroup) { logger.Socket.Info("Starting Unix socket server...") From 013c1f23e7a380d6cfc784934a340d0e593f17f8 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:39:44 +0200 Subject: [PATCH 41/93] renamed UIMod folder to SSUI --- .docker/Dockerfile | 6 +-- .docker/compose.yml | 2 +- .gitignore | 16 +++--- .../onboard_bundled/assets/apiinfo.html | 0 .../onboard_bundled/assets/css/apiinfo.css | 0 .../onboard_bundled/assets/css/background.css | 0 .../onboard_bundled/assets/css/base.css | 0 .../onboard_bundled/assets/css/components.css | 0 .../onboard_bundled/assets/css/config.css | 0 .../assets/css/detectionmanager.css | 0 .../onboard_bundled/assets/css/flags.css | 0 .../onboard_bundled/assets/css/home.css | 0 .../assets/css/info-notice.css | 0 .../onboard_bundled/assets/css/mobile.css | 0 .../onboard_bundled/assets/css/popup.css | 0 .../onboard_bundled/assets/css/sscm.css | 0 .../onboard_bundled/assets/css/style.css | 0 .../onboard_bundled/assets/css/tabs.css | 0 .../onboard_bundled/assets/css/variables.css | 0 .../onboard_bundled/assets/favicon.ico | Bin .../onboard_bundled/assets/flags/de.webp | Bin .../onboard_bundled/assets/flags/en.webp | Bin .../onboard_bundled/assets/flags/sv.webp | Bin .../assets/js/console-manager.js | 0 .../assets/js/detectionmanager.js | 0 .../onboard_bundled/assets/js/main.js | 0 .../onboard_bundled/assets/js/popup.js | 0 .../onboard_bundled/assets/js/server-api.js | 0 .../onboard_bundled/assets/js/sscm.js | 0 .../onboard_bundled/assets/js/ui-utils.js | 0 .../assets/js/world-gen-config.js | 0 .../assets/playerimages/anna.webp | Bin .../assets/playerimages/dan.webp | Bin .../assets/playerimages/darragh.webp | Bin .../assets/playerimages/david.webp | Bin .../assets/playerimages/dean.webp | Bin .../assets/playerimages/garrison.webp | Bin .../assets/playerimages/ivette.webp | Bin .../assets/playerimages/john.webp | Bin .../assets/playerimages/julia.webp | Bin .../assets/playerimages/ove.webp | Bin .../assets/playerimages/pierre.webp | Bin .../assets/playerimages/rolf.webp | Bin .../assets/playerimages/ronald.webp | Bin .../onboard_bundled/assets/stationeers.webp | Bin .../detectionmanager/detectionmanager.html | 0 .../onboard_bundled/localization/de-DE.json | 0 .../onboard_bundled/localization/en-US.json | 0 .../onboard_bundled/localization/sv-SE.json | 0 .../onboard_bundled/twoboxform/twoboxform.css | 0 .../twoboxform/twoboxform.html | 0 .../onboard_bundled/twoboxform/twoboxform.js | 0 .../onboard_bundled/ui/config.html | 0 {UIMod => SSUI}/onboard_bundled/ui/index.html | 0 UIMod/onboard_bundled/scripts/autostart.ps1 | 51 ------------------ UIMod/onboard_bundled/scripts/autostart.sh | 3 -- frontend/electron-builder.yml | 4 +- frontend/package.json | 2 +- frontend/vite.config.js | 2 +- server.go | 2 +- src/api/TwoBoxForm.go | 2 +- src/api/configpage.go | 2 +- src/api/detectionmanagerpage.go | 2 +- src/api/indexpage.go | 2 +- src/api/routes.go | 6 +-- src/api/svelteui.go | 2 +- src/cli/runtimecommands.go | 7 +-- src/config/getters.go | 4 +- src/config/vars.go | 26 ++++----- src/core/loader/afterstart.go | 2 +- src/core/loader/helpers.go | 2 +- src/core/security/tls.go | 2 +- src/localization/localization.go | 2 +- src/setup/cleanup.go | 4 +- src/setup/install.go | 24 ++++----- 75 files changed, 62 insertions(+), 115 deletions(-) rename {UIMod => SSUI}/onboard_bundled/assets/apiinfo.html (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/apiinfo.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/background.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/base.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/components.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/config.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/detectionmanager.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/flags.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/home.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/info-notice.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/mobile.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/popup.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/sscm.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/style.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/tabs.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/css/variables.css (100%) rename {UIMod => SSUI}/onboard_bundled/assets/favicon.ico (100%) rename {UIMod => SSUI}/onboard_bundled/assets/flags/de.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/flags/en.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/flags/sv.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/console-manager.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/detectionmanager.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/main.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/popup.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/server-api.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/sscm.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/ui-utils.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/js/world-gen-config.js (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/anna.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/dan.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/darragh.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/david.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/dean.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/garrison.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/ivette.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/john.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/julia.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/ove.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/pierre.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/rolf.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/playerimages/ronald.webp (100%) rename {UIMod => SSUI}/onboard_bundled/assets/stationeers.webp (100%) rename {UIMod => SSUI}/onboard_bundled/detectionmanager/detectionmanager.html (100%) rename {UIMod => SSUI}/onboard_bundled/localization/de-DE.json (100%) rename {UIMod => SSUI}/onboard_bundled/localization/en-US.json (100%) rename {UIMod => SSUI}/onboard_bundled/localization/sv-SE.json (100%) rename {UIMod => SSUI}/onboard_bundled/twoboxform/twoboxform.css (100%) rename {UIMod => SSUI}/onboard_bundled/twoboxform/twoboxform.html (100%) rename {UIMod => SSUI}/onboard_bundled/twoboxform/twoboxform.js (100%) rename {UIMod => SSUI}/onboard_bundled/ui/config.html (100%) rename {UIMod => SSUI}/onboard_bundled/ui/index.html (100%) delete mode 100644 UIMod/onboard_bundled/scripts/autostart.ps1 delete mode 100644 UIMod/onboard_bundled/scripts/autostart.sh diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 9bd45c49..e6bf47a4 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -10,7 +10,7 @@ COPY ./frontend/package.json ./frontend/package-lock.json* ./ RUN npm install COPY ./frontend/ ./ -# Build Svelte UI into ../UIMod/onboard_bundled/v2 (as configured in vite.config.js) +# Build Svelte UI into ../SSUI/onboard_bundled/v2 (as configured in vite.config.js) RUN npm run build ############ @@ -26,9 +26,9 @@ RUN go mod download COPY . ./ -COPY --from=frontend-builder /src/UIMod /src/UIMod +COPY --from=frontend-builder /src/SSUI /src/SSUI -# Build the server (embed will include UIMod/* at build time) +# Build the server (embed will include SSUI/* at build time) RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/StationeersServerUI ./server.go ############ diff --git a/.docker/compose.yml b/.docker/compose.yml index a57d4413..920705af 100644 --- a/.docker/compose.yml +++ b/.docker/compose.yml @@ -24,7 +24,7 @@ services: volumes: - app-data:/app - ./saves:/app/saves:rw - - ./UIMod:/app/UIMod:rw + - ./SSUI:/app/SSUI:rw restart: unless-stopped volumes: diff --git a/.gitignore b/.gitignore index afe048f0..cb39947c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,12 +16,12 @@ rocketstation_DedicatedServer* /saves/* .github/workflows/nightly-sync.yml repos.md -UIMod/*.pem +SSUI/*.pem build/StationeersServerControl* Blacklist.txt -UIMod/detectionmanager/customdetections.json -UIMod/tls/cert.pem -UIMod/tls/key.pem +SSUI/detectionmanager/customdetections.json +SSUI/tls/cert.pem +SSUI/tls/key.pem steamapps/** steamcmd/** Steam/** @@ -34,14 +34,14 @@ BepInEx/** run_bepinex.sh debug.log modconfig.xml -UIMod/config/config.json +SSUI/config/config.json C:/custom/file.txt -UIMod/config/customdetections.json +SSUI/config/customdetections.json winhttp.dll autostart* __debug_bin* frontend/node_modules frontend/dist frontend/build -UIMod/onboard_bundled/v2 -UIMod/runfiles/** +SSUI/onboard_bundled/v2 +SSUI/runfiles/** diff --git a/UIMod/onboard_bundled/assets/apiinfo.html b/SSUI/onboard_bundled/assets/apiinfo.html similarity index 100% rename from UIMod/onboard_bundled/assets/apiinfo.html rename to SSUI/onboard_bundled/assets/apiinfo.html diff --git a/UIMod/onboard_bundled/assets/css/apiinfo.css b/SSUI/onboard_bundled/assets/css/apiinfo.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/apiinfo.css rename to SSUI/onboard_bundled/assets/css/apiinfo.css diff --git a/UIMod/onboard_bundled/assets/css/background.css b/SSUI/onboard_bundled/assets/css/background.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/background.css rename to SSUI/onboard_bundled/assets/css/background.css diff --git a/UIMod/onboard_bundled/assets/css/base.css b/SSUI/onboard_bundled/assets/css/base.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/base.css rename to SSUI/onboard_bundled/assets/css/base.css diff --git a/UIMod/onboard_bundled/assets/css/components.css b/SSUI/onboard_bundled/assets/css/components.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/components.css rename to SSUI/onboard_bundled/assets/css/components.css diff --git a/UIMod/onboard_bundled/assets/css/config.css b/SSUI/onboard_bundled/assets/css/config.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/config.css rename to SSUI/onboard_bundled/assets/css/config.css diff --git a/UIMod/onboard_bundled/assets/css/detectionmanager.css b/SSUI/onboard_bundled/assets/css/detectionmanager.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/detectionmanager.css rename to SSUI/onboard_bundled/assets/css/detectionmanager.css diff --git a/UIMod/onboard_bundled/assets/css/flags.css b/SSUI/onboard_bundled/assets/css/flags.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/flags.css rename to SSUI/onboard_bundled/assets/css/flags.css diff --git a/UIMod/onboard_bundled/assets/css/home.css b/SSUI/onboard_bundled/assets/css/home.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/home.css rename to SSUI/onboard_bundled/assets/css/home.css diff --git a/UIMod/onboard_bundled/assets/css/info-notice.css b/SSUI/onboard_bundled/assets/css/info-notice.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/info-notice.css rename to SSUI/onboard_bundled/assets/css/info-notice.css diff --git a/UIMod/onboard_bundled/assets/css/mobile.css b/SSUI/onboard_bundled/assets/css/mobile.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/mobile.css rename to SSUI/onboard_bundled/assets/css/mobile.css diff --git a/UIMod/onboard_bundled/assets/css/popup.css b/SSUI/onboard_bundled/assets/css/popup.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/popup.css rename to SSUI/onboard_bundled/assets/css/popup.css diff --git a/UIMod/onboard_bundled/assets/css/sscm.css b/SSUI/onboard_bundled/assets/css/sscm.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/sscm.css rename to SSUI/onboard_bundled/assets/css/sscm.css diff --git a/UIMod/onboard_bundled/assets/css/style.css b/SSUI/onboard_bundled/assets/css/style.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/style.css rename to SSUI/onboard_bundled/assets/css/style.css diff --git a/UIMod/onboard_bundled/assets/css/tabs.css b/SSUI/onboard_bundled/assets/css/tabs.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/tabs.css rename to SSUI/onboard_bundled/assets/css/tabs.css diff --git a/UIMod/onboard_bundled/assets/css/variables.css b/SSUI/onboard_bundled/assets/css/variables.css similarity index 100% rename from UIMod/onboard_bundled/assets/css/variables.css rename to SSUI/onboard_bundled/assets/css/variables.css diff --git a/UIMod/onboard_bundled/assets/favicon.ico b/SSUI/onboard_bundled/assets/favicon.ico similarity index 100% rename from UIMod/onboard_bundled/assets/favicon.ico rename to SSUI/onboard_bundled/assets/favicon.ico diff --git a/UIMod/onboard_bundled/assets/flags/de.webp b/SSUI/onboard_bundled/assets/flags/de.webp similarity index 100% rename from UIMod/onboard_bundled/assets/flags/de.webp rename to SSUI/onboard_bundled/assets/flags/de.webp diff --git a/UIMod/onboard_bundled/assets/flags/en.webp b/SSUI/onboard_bundled/assets/flags/en.webp similarity index 100% rename from UIMod/onboard_bundled/assets/flags/en.webp rename to SSUI/onboard_bundled/assets/flags/en.webp diff --git a/UIMod/onboard_bundled/assets/flags/sv.webp b/SSUI/onboard_bundled/assets/flags/sv.webp similarity index 100% rename from UIMod/onboard_bundled/assets/flags/sv.webp rename to SSUI/onboard_bundled/assets/flags/sv.webp diff --git a/UIMod/onboard_bundled/assets/js/console-manager.js b/SSUI/onboard_bundled/assets/js/console-manager.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/console-manager.js rename to SSUI/onboard_bundled/assets/js/console-manager.js diff --git a/UIMod/onboard_bundled/assets/js/detectionmanager.js b/SSUI/onboard_bundled/assets/js/detectionmanager.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/detectionmanager.js rename to SSUI/onboard_bundled/assets/js/detectionmanager.js diff --git a/UIMod/onboard_bundled/assets/js/main.js b/SSUI/onboard_bundled/assets/js/main.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/main.js rename to SSUI/onboard_bundled/assets/js/main.js diff --git a/UIMod/onboard_bundled/assets/js/popup.js b/SSUI/onboard_bundled/assets/js/popup.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/popup.js rename to SSUI/onboard_bundled/assets/js/popup.js diff --git a/UIMod/onboard_bundled/assets/js/server-api.js b/SSUI/onboard_bundled/assets/js/server-api.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/server-api.js rename to SSUI/onboard_bundled/assets/js/server-api.js diff --git a/UIMod/onboard_bundled/assets/js/sscm.js b/SSUI/onboard_bundled/assets/js/sscm.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/sscm.js rename to SSUI/onboard_bundled/assets/js/sscm.js diff --git a/UIMod/onboard_bundled/assets/js/ui-utils.js b/SSUI/onboard_bundled/assets/js/ui-utils.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/ui-utils.js rename to SSUI/onboard_bundled/assets/js/ui-utils.js diff --git a/UIMod/onboard_bundled/assets/js/world-gen-config.js b/SSUI/onboard_bundled/assets/js/world-gen-config.js similarity index 100% rename from UIMod/onboard_bundled/assets/js/world-gen-config.js rename to SSUI/onboard_bundled/assets/js/world-gen-config.js diff --git a/UIMod/onboard_bundled/assets/playerimages/anna.webp b/SSUI/onboard_bundled/assets/playerimages/anna.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/anna.webp rename to SSUI/onboard_bundled/assets/playerimages/anna.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/dan.webp b/SSUI/onboard_bundled/assets/playerimages/dan.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/dan.webp rename to SSUI/onboard_bundled/assets/playerimages/dan.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/darragh.webp b/SSUI/onboard_bundled/assets/playerimages/darragh.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/darragh.webp rename to SSUI/onboard_bundled/assets/playerimages/darragh.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/david.webp b/SSUI/onboard_bundled/assets/playerimages/david.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/david.webp rename to SSUI/onboard_bundled/assets/playerimages/david.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/dean.webp b/SSUI/onboard_bundled/assets/playerimages/dean.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/dean.webp rename to SSUI/onboard_bundled/assets/playerimages/dean.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/garrison.webp b/SSUI/onboard_bundled/assets/playerimages/garrison.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/garrison.webp rename to SSUI/onboard_bundled/assets/playerimages/garrison.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/ivette.webp b/SSUI/onboard_bundled/assets/playerimages/ivette.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/ivette.webp rename to SSUI/onboard_bundled/assets/playerimages/ivette.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/john.webp b/SSUI/onboard_bundled/assets/playerimages/john.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/john.webp rename to SSUI/onboard_bundled/assets/playerimages/john.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/julia.webp b/SSUI/onboard_bundled/assets/playerimages/julia.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/julia.webp rename to SSUI/onboard_bundled/assets/playerimages/julia.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/ove.webp b/SSUI/onboard_bundled/assets/playerimages/ove.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/ove.webp rename to SSUI/onboard_bundled/assets/playerimages/ove.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/pierre.webp b/SSUI/onboard_bundled/assets/playerimages/pierre.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/pierre.webp rename to SSUI/onboard_bundled/assets/playerimages/pierre.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/rolf.webp b/SSUI/onboard_bundled/assets/playerimages/rolf.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/rolf.webp rename to SSUI/onboard_bundled/assets/playerimages/rolf.webp diff --git a/UIMod/onboard_bundled/assets/playerimages/ronald.webp b/SSUI/onboard_bundled/assets/playerimages/ronald.webp similarity index 100% rename from UIMod/onboard_bundled/assets/playerimages/ronald.webp rename to SSUI/onboard_bundled/assets/playerimages/ronald.webp diff --git a/UIMod/onboard_bundled/assets/stationeers.webp b/SSUI/onboard_bundled/assets/stationeers.webp similarity index 100% rename from UIMod/onboard_bundled/assets/stationeers.webp rename to SSUI/onboard_bundled/assets/stationeers.webp diff --git a/UIMod/onboard_bundled/detectionmanager/detectionmanager.html b/SSUI/onboard_bundled/detectionmanager/detectionmanager.html similarity index 100% rename from UIMod/onboard_bundled/detectionmanager/detectionmanager.html rename to SSUI/onboard_bundled/detectionmanager/detectionmanager.html diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/SSUI/onboard_bundled/localization/de-DE.json similarity index 100% rename from UIMod/onboard_bundled/localization/de-DE.json rename to SSUI/onboard_bundled/localization/de-DE.json diff --git a/UIMod/onboard_bundled/localization/en-US.json b/SSUI/onboard_bundled/localization/en-US.json similarity index 100% rename from UIMod/onboard_bundled/localization/en-US.json rename to SSUI/onboard_bundled/localization/en-US.json diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/SSUI/onboard_bundled/localization/sv-SE.json similarity index 100% rename from UIMod/onboard_bundled/localization/sv-SE.json rename to SSUI/onboard_bundled/localization/sv-SE.json diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.css b/SSUI/onboard_bundled/twoboxform/twoboxform.css similarity index 100% rename from UIMod/onboard_bundled/twoboxform/twoboxform.css rename to SSUI/onboard_bundled/twoboxform/twoboxform.css diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.html b/SSUI/onboard_bundled/twoboxform/twoboxform.html similarity index 100% rename from UIMod/onboard_bundled/twoboxform/twoboxform.html rename to SSUI/onboard_bundled/twoboxform/twoboxform.html diff --git a/UIMod/onboard_bundled/twoboxform/twoboxform.js b/SSUI/onboard_bundled/twoboxform/twoboxform.js similarity index 100% rename from UIMod/onboard_bundled/twoboxform/twoboxform.js rename to SSUI/onboard_bundled/twoboxform/twoboxform.js diff --git a/UIMod/onboard_bundled/ui/config.html b/SSUI/onboard_bundled/ui/config.html similarity index 100% rename from UIMod/onboard_bundled/ui/config.html rename to SSUI/onboard_bundled/ui/config.html diff --git a/UIMod/onboard_bundled/ui/index.html b/SSUI/onboard_bundled/ui/index.html similarity index 100% rename from UIMod/onboard_bundled/ui/index.html rename to SSUI/onboard_bundled/ui/index.html diff --git a/UIMod/onboard_bundled/scripts/autostart.ps1 b/UIMod/onboard_bundled/scripts/autostart.ps1 deleted file mode 100644 index e7e67420..00000000 --- a/UIMod/onboard_bundled/scripts/autostart.ps1 +++ /dev/null @@ -1,51 +0,0 @@ -# Path to this script file -$scriptPath = $MyInvocation.MyCommand.Path - -# Path to user's startup folder -$startupFolder = [Environment]::GetFolderPath("Startup") - -# Shortcut name in startup -$shortcutName = "Start-StationeersServerUI.lnk" -$shortcutPath = Join-Path $startupFolder $shortcutName - -# Function to create shortcut -function New-Shortcut { - param ( - [string]$targetPath, - [string]$shortcutPath - ) - - $shell = New-Object -ComObject WScript.Shell - $shortcut = $shell.CreateShortcut($shortcutPath) - # Set the target to powershell.exe and pass the script as an argument - $shortcut.TargetPath = "powershell.exe" - $shortcut.Arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$targetPath`"" - $shortcut.WorkingDirectory = Split-Path $targetPath - $shortcut.Save() -} - -# Check if shortcut exists in startup folder -if (-not (Test-Path $shortcutPath)) { - Write-Output "Shortcut not found in Startup folder. Creating shortcut to enable autostart..." - New-Shortcut -targetPath $scriptPath -shortcutPath $shortcutPath - Write-Output "Shortcut created. You may need to restart your session to apply autostart." - Read-Host "Press Enter to exit" - exit -} - -# Folder where the executables are located (folder of this script) -$exeFolder = Split-Path -Parent $scriptPath - -# Find latest StationeersServerControl*.exe by last write time as old executables are prefixed _old anyway -$latestExe = Get-ChildItem -Path $exeFolder -Filter "StationeersServerControl*.exe" | - Sort-Object LastWriteTime -Descending | - Select-Object -First 1 - -if ($null -eq $latestExe) { - Write-Error "No executable found to start in $exeFolder" - Read-Host "Press Enter to exit" - exit 1 -} - -Write-Output "Starting $($latestExe.FullName)..." -Start-Process -FilePath $latestExe.FullName \ No newline at end of file diff --git a/UIMod/onboard_bundled/scripts/autostart.sh b/UIMod/onboard_bundled/scripts/autostart.sh deleted file mode 100644 index b3d220b4..00000000 --- a/UIMod/onboard_bundled/scripts/autostart.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -# not yet implemented \ No newline at end of file diff --git a/frontend/electron-builder.yml b/frontend/electron-builder.yml index 5b261e84..bc501d99 100644 --- a/frontend/electron-builder.yml +++ b/frontend/electron-builder.yml @@ -9,8 +9,8 @@ files: - "package.json" - "main.cjs" extraResources: - - from: "../UIMod/onboard_bundled/v2/" - to: "UIMod/onboard_bundled/v2/" + - from: "../SSUI/onboard_bundled/v2/" + to: "SSUI/onboard_bundled/v2/" win: target: nsis icon: ../media/logo.png diff --git a/frontend/package.json b/frontend/package.json index eb5f3888..e0e63784 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "name": "steamserverui", "main": "main.cjs", "private": true, - "version": "v5.7.1", + "version": "v7.0.1", "description": "Svelte UI Interface for Steam Server UI (SSUI) Backend", "author": { "name": "JacksonTheMaster", diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 90f788e8..79f46f47 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -5,7 +5,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte' export default defineConfig({ plugins: [svelte()], build: { - outDir: '../UIMod/onboard_bundled/v2', // Change output directory to ../dist + outDir: '../SSUI/onboard_bundled/v2', // Change output directory to ../dist rollupOptions: { output: { // Set the name of the JS bundle diff --git a/server.go b/server.go index c6f72e8b..b9abcf5d 100644 --- a/server.go +++ b/server.go @@ -32,7 +32,7 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/setup" ) -//go:embed UIMod/onboard_bundled +//go:embed SSUI/onboard_bundled var v1uiFS embed.FS func main() { diff --git a/src/api/TwoBoxForm.go b/src/api/TwoBoxForm.go index 7ef16e2b..86ff770e 100644 --- a/src/api/TwoBoxForm.go +++ b/src/api/TwoBoxForm.go @@ -58,7 +58,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { Steps []Step } - twoboxformAssetsFS, err := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") + twoboxformAssetsFS, err := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/twoboxform") if err != nil { logger.Web.Error("Failed to get bundled FS") http.Error(w, "Internal Server Error", http.StatusInternalServerError) diff --git a/src/api/configpage.go b/src/api/configpage.go index 870c6cbe..cef76a49 100644 --- a/src/api/configpage.go +++ b/src/api/configpage.go @@ -13,7 +13,7 @@ import ( func ServeConfigPage(w http.ResponseWriter, r *http.Request) { - htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/ui") + htmlFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/ui") if err != nil { http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) return diff --git a/src/api/detectionmanagerpage.go b/src/api/detectionmanagerpage.go index 19ff5a5b..189658f5 100644 --- a/src/api/detectionmanagerpage.go +++ b/src/api/detectionmanagerpage.go @@ -10,7 +10,7 @@ import ( ) func ServeDetectionManager(w http.ResponseWriter, r *http.Request) { - detectionmanagerFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/detectionmanager") + detectionmanagerFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/detectionmanager") if err != nil { http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) return diff --git a/src/api/indexpage.go b/src/api/indexpage.go index db6adcdf..305fb018 100644 --- a/src/api/indexpage.go +++ b/src/api/indexpage.go @@ -11,7 +11,7 @@ import ( ) func ServeIndex(w http.ResponseWriter, r *http.Request) { - htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/ui") + htmlFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/ui") if err != nil { http.Error(w, "Error accessing Virt FS: "+err.Error(), http.StatusInternalServerError) return diff --git a/src/api/routes.go b/src/api/routes.go index d6ae6d11..1b718a1a 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -17,7 +17,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { mux := http.NewServeMux() // Use a mux to apply middleware globally // Unprotected auth routes - twoboxformAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/twoboxform") + twoboxformAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/twoboxform") mux.Handle("/twoboxform/", http.StripPrefix("/twoboxform/", http.FileServer(http.FS(twoboxformAssetsFS)))) mux.HandleFunc("/auth/login", LoginHandler) // Token issuer mux.HandleFunc("/auth/logout", LogoutHandler) @@ -26,7 +26,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { // Protected routes (wrapped with middleware) protectedMux := http.NewServeMux() - legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "UIMod/onboard_bundled/assets") + legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/assets") protectedMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(legacyAssetsFS)))) protectedMux.HandleFunc("/config", ServeConfigPage) @@ -35,7 +35,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { // --- SVELTE UI --- protectedMux.HandleFunc("/v2", ServeSvelteUI) - svelteAssetsFS, _ := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/v2/assets") + svelteAssetsFS, _ := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/v2/assets") protectedMux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(svelteAssetsFS)))) protectedMux.HandleFunc("/api/v2/loader/reloadbackend", HandleReloadAll) diff --git a/src/api/svelteui.go b/src/api/svelteui.go index 2475dc95..1bfd3631 100644 --- a/src/api/svelteui.go +++ b/src/api/svelteui.go @@ -15,7 +15,7 @@ import ( var reloadMu sync.Mutex func ServeSvelteUI(w http.ResponseWriter, r *http.Request) { - htmlFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/v2") + htmlFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/v2") if err != nil { http.Error(w, "Error accessing Svelte UI: "+err.Error(), http.StatusInternalServerError) return diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 8c617127..2c10e93b 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -14,6 +14,7 @@ import ( "path/filepath" "runtime" "sort" + "strings" "sync" "time" @@ -253,7 +254,7 @@ func supportPackage() { zw := zip.NewWriter(zipFile) defer zw.Close() - filepath.Walk("./UIMod/logs", func(p string, i os.FileInfo, err error) error { + filepath.Walk("./SSUI/logs", func(p string, i os.FileInfo, err error) error { if err != nil || i.IsDir() { return nil } @@ -264,7 +265,7 @@ func supportPackage() { return nil }) - configData, _ := os.ReadFile("./UIMod/config/config.json") + configData, _ := os.ReadFile("./SSUI/config/config.json") var configMap map[string]interface{} if err := json.Unmarshal(configData, &configMap); err != nil { @@ -284,7 +285,7 @@ func supportPackage() { } // Write sanitized config to zip - w, _ := zw.Create("UIMod/config/config.json") + w, _ := zw.Create("SSUI/config/config.json") if _, err := w.Write(sanitizedConfig); err != nil { logger.Core.Error("Failed to write sanitized config to support package") diff --git a/src/config/getters.go b/src/config/getters.go index 80b57d0b..ba951bea 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -436,10 +436,10 @@ func GetTLSKeyPath() string { return TLSKeyPath } -func GetUIModFolder() string { +func GetSSUIFolder() string { ConfigMu.RLock() defer ConfigMu.RUnlock() - return UIModFolder + return SSUIFolder } func GetMaxSSEConnections() int { diff --git a/src/config/vars.go b/src/config/vars.go index 3b75400a..e9bcb93b 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -137,21 +137,21 @@ var ( // File paths var ( - TLSCertPath = "./UIMod/tls/cert.pem" - TLSKeyPath = "./UIMod/tls/key.pem" - ConfigPath = "./UIMod/config/config.json" - CustomDetectionsFilePath = "./UIMod/config/customdetections.json" - LogFolder = "./UIMod/logs/" - UIModFolder = "./UIMod/" - TwoBoxFormFolder = "./UIMod/twoboxform/" - ConfigHtmlPath = "./UIMod/ui/config.html" - DetectionManagerHtmlPath = "./UIMod/ui/detectionmanager.html" - TwoBoxFormHtmlPath = "./UIMod/twoboxform/twoboxform.html" - IndexHtmlPath = "./UIMod/ui/index.html" - SSCMWebDir = "./UIMod/sscm/" + TLSCertPath = "./SSUI/tls/cert.pem" + TLSKeyPath = "./SSUI/tls/key.pem" + ConfigPath = "./SSUI/config/config.json" + CustomDetectionsFilePath = "./SSUI/config/customdetections.json" + LogFolder = "./SSUI/logs/" + SSUIFolder = "./SSUI/" + TwoBoxFormFolder = "./SSUI/twoboxform/" + ConfigHtmlPath = "./SSUI/ui/config.html" + DetectionManagerHtmlPath = "./SSUI/ui/detectionmanager.html" + TwoBoxFormHtmlPath = "./SSUI/twoboxform/twoboxform.html" + IndexHtmlPath = "./SSUI/ui/index.html" + SSCMWebDir = "./SSUI/sscm/" SSCMFilePath = "./BepInEx/plugins/SSCM/SSCM.socket" SSCMPluginDir = "./BepInEx/plugins/SSCM/" - RunFilesFolder = "./UIMod/runfiles/" + RunFilesFolder = "./SSUI/runfiles/" ) // Bundled Assets diff --git a/src/core/loader/afterstart.go b/src/core/loader/afterstart.go index b5dc2d51..a0d21007 100644 --- a/src/core/loader/afterstart.go +++ b/src/core/loader/afterstart.go @@ -14,7 +14,7 @@ func AfterStartComplete(wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() config.SetSaveConfig() // Save config after startup through setters - err := setup.CleanUpOldUIModFolderFiles() + err := setup.CleanUpOldSSUIFolderFiles() if err != nil { logger.Core.Error("AfterStartComplete: Failed to clean up old pre-v5.5 UI mod folder files: " + err.Error()) } diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index 540489c7..5cff29b7 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -143,7 +143,7 @@ func PrintConfigDetails(logLevel ...string) { ui := map[string]string{ "SSUIIdentifier": config.GetSSUIIdentifier(), "SSUIWebPort": config.GetSSUIWebPort(), - "UIModFolder": config.GetUIModFolder(), + "SSUIFolder": config.GetSSUIFolder(), "MaxSSEConnections": fmt.Sprintf("%d", config.GetMaxSSEConnections()), "SSEMessageBufferSize": fmt.Sprintf("%d", config.GetSSEMessageBufferSize()), } diff --git a/src/core/security/tls.go b/src/core/security/tls.go index e478ef1e..5dcae2b4 100644 --- a/src/core/security/tls.go +++ b/src/core/security/tls.go @@ -31,7 +31,7 @@ func EnsureTLSCerts() error { keyExists := fileExists(keyPath) logger.Security.Debug(fmt.Sprintf("Cert exists: %t, Key exists: %t", certExists, keyExists)) - tlsDir := config.GetUIModFolder() + "tls/" + tlsDir := config.GetSSUIFolder() + "tls/" if _, err := os.Stat(tlsDir); os.IsNotExist(err) { logger.Security.Debug("TLS directory doesn't exist, creating...") diff --git a/src/localization/localization.go b/src/localization/localization.go index 7a0e417d..29ad4bcb 100644 --- a/src/localization/localization.go +++ b/src/localization/localization.go @@ -32,7 +32,7 @@ func loadTranslations() { // Clear existing translations translations = make(map[string]map[string]string) - virtFS, err := fs.Sub(config.V1UIFS, "UIMod/onboard_bundled/localization") + virtFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/localization") if err != nil { logger.Localization.Error("Failed to access virtual filesystem: " + err.Error()) return diff --git a/src/setup/cleanup.go b/src/setup/cleanup.go index 6ef81c57..91ef45d6 100644 --- a/src/setup/cleanup.go +++ b/src/setup/cleanup.go @@ -11,8 +11,8 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) -func CleanUpOldUIModFolderFiles() error { - uiModFolder := config.GetUIModFolder() +func CleanUpOldSSUIFolderFiles() error { + uiModFolder := config.GetSSUIFolder() customdetectionsSourceFile := filepath.Join(uiModFolder, "detectionmanager", "customdetections.json") customdetectionsDestinationFile := config.GetCustomDetectionsFilePath() oldUiFolder := filepath.Join(uiModFolder, "ui") // used to test if we need clean up from a structure before v5.5 (since we now have embedded assets) diff --git a/src/setup/install.go b/src/setup/install.go index 6cd8fb94..9200c949 100644 --- a/src/setup/install.go +++ b/src/setup/install.go @@ -31,10 +31,10 @@ func Install(wg *sync.WaitGroup) { logger.Install.Error("❌Update check went sideways: " + err.Error()) } - // Step 1: Check and download the UIMod folder contents - logger.Install.Debug("🔄Checking UIMod folder...") - CheckAndDownloadUIMod() - logger.Install.Debug("✅UIMod folder setup complete.") + // Step 1: Check and download the SSUI folder contents + logger.Install.Debug("🔄Checking SSUI folder...") + CheckAndDownloadSSUI() + logger.Install.Debug("✅SSUI folder setup complete.") // Step 2: Check for Blacklist.txt and create it if it doesn't exist logger.Install.Info("🔄Checking for Blacklist.txt...") checkAndCreateBlacklist() @@ -42,10 +42,10 @@ func Install(wg *sync.WaitGroup) { logger.Install.Info("✅Setup complete!") } -func CheckAndDownloadUIMod() { - uiModDir := config.GetUIModFolder() - configDir := config.GetUIModFolder() + "config/" - tlsDir := config.GetUIModFolder() + "tls/" +func CheckAndDownloadSSUI() { + uiModDir := config.GetSSUIFolder() + configDir := config.GetSSUIFolder() + "config/" + tlsDir := config.GetSSUIFolder() + "tls/" requiredDirs := []string{uiModDir, configDir} @@ -62,7 +62,7 @@ func CheckAndDownloadUIMod() { // NOTE: Now empty as files are now embedded in the executable. Kept this structure for future use. // UI - commented out since files are embedded, left here for reference in case we need this funcitonality again - // "ui/config.html": "https://raw.githubusercontent.com/SteamServerUI/SteamServerUI/{branch}/UIMod/ui/config.html", + // "ui/config.html": "https://raw.githubusercontent.com/SteamServerUI/SteamServerUI/{branch}/SSUI/ui/config.html", } createRequiredDirs(requiredDirs) @@ -88,7 +88,7 @@ func CheckAndDownloadUIMod() { logger.Install.Debug(fmt.Sprintf("IsUpdateEnabled: %v", config.GetIsUpdateEnabled())) logger.Install.Debug(fmt.Sprintf("IsFirstTimeSetup: %v", config.GetIsFirstTimeSetup())) if config.GetIsUpdateEnabled() { - logger.Install.Info("🔍Validating UIMod files for updates...") + logger.Install.Info("🔍Validating SSUI files for updates...") if config.GetBranch() == "release" || config.GetBranch() == "Release" { downloadBranch = "main" updateFilesIfDifferent(files) @@ -97,7 +97,7 @@ func CheckAndDownloadUIMod() { updateFilesIfDifferent(files) } } else { - logger.Install.Info("♻️Folder ./UIMod already exists. Updates disabled, skipping validation.") + logger.Install.Info("♻️Folder ./SSUI already exists. Updates disabled, skipping validation.") } } } @@ -163,7 +163,7 @@ func checkAndUpdateFile(filepath, url string) { } // Extract the necessary parts from URL to build the API call - // Example URL: https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/UIMod/index.html + // Example URL: https://raw.githubusercontent.com/JacksonTheMaster/StationeersServerUI/main/SSUI/index.html urlParts := strings.Split(url, "/") if len(urlParts) < 7 { logger.Install.Error("❌Invalid URL format: " + url) From 35f411aba26df1e1dfbb3f918c479873c6defee6 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com> Date: Sat, 11 Oct 2025 03:45:32 +0200 Subject: [PATCH 42/93] fix github workflows after renames --- .github/workflows/auto-release.yaml | 4 ++-- .github/workflows/test-build.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-release.yaml b/.github/workflows/auto-release.yaml index b36a7157..5252c402 100644 --- a/.github/workflows/auto-release.yaml +++ b/.github/workflows/auto-release.yaml @@ -39,7 +39,7 @@ jobs: - name: Verify build output run: | cd build - if ls StationeersServerControl*; then + if ls SteamServerUI*; then echo "Build succeeded: Executable found." else echo "Build failed: No executable found." @@ -108,7 +108,7 @@ jobs: prerelease: false tag_name: ${{ steps.check_tag.outputs.tag }} name: "Release ${{ steps.check_tag.outputs.tag }}" - files: ./build/StationeersServerControl* + files: ./build/SteamServerUI* fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index e26bd219..10f162f3 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -32,7 +32,7 @@ jobs: - name: Verify build output run: | cd build - if ls StationeersServerControl*; then + if ls SteamServerUI*; then echo "Build succeeded: Executable found." else echo "Build failed: No executable found." @@ -45,4 +45,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: StationeersServerControl - path: ./build/StationeersServerControl* \ No newline at end of file + path: ./build/SteamServerUI* \ No newline at end of file From e1dcf5514df04ab1fcbd3885bbb4c7e8eea32075 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 14 Oct 2025 02:39:12 +0200 Subject: [PATCH 43/93] refactor: move web's auth handlers to httpauth package and update route definitions accordingly --- src/api/httpauth/activate.go | 55 ++++++ src/api/httpauth/login.go | 62 +++++++ src/api/httpauth/logout.go | 32 ++++ src/api/httpauth/register.go | 52 ++++++ src/api/login.go | 242 --------------------------- src/api/middleware/authmiddleware.go | 76 +++++++++ src/api/routes.go | 11 +- src/api/start.go | 3 +- 8 files changed, 285 insertions(+), 248 deletions(-) create mode 100644 src/api/httpauth/activate.go create mode 100644 src/api/httpauth/login.go create mode 100644 src/api/httpauth/logout.go create mode 100644 src/api/httpauth/register.go delete mode 100644 src/api/login.go create mode 100644 src/api/middleware/authmiddleware.go diff --git a/src/api/httpauth/activate.go b/src/api/httpauth/activate.go new file mode 100644 index 00000000..a807a603 --- /dev/null +++ b/src/api/httpauth/activate.go @@ -0,0 +1,55 @@ +package httpauth + +import ( + "encoding/json" + "net/http" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" +) + +// ActivateAuthHandler (ex. SetupFinalizeHandler) marks setup as complete +func ActivateAuthHandler(w http.ResponseWriter, r *http.Request) { + + //check if users map is nil or empty + if len(config.GetUsers()) == 0 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "No users registered - cannot finalize setup at this time. You should really enable authentication - or click 'Skip authentication'"}) + return + } + + // Load existing config to update it + newConfig, err := config.LoadConfig() + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to load config"}) + return + } + + // Mark setup as complete and enable auth + config.SetIsFirstTimeSetup(false) + isTrue := true + newConfig.AuthEnabled = &isTrue // Set the pointer to true + + // Save the updated config + err = configchanger.SaveConfig(newConfig) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to save config"}) + return + } + + logger.Web.Info("User Setup finalized successfully") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Setup finalized successfully", + "restart_hint": "You will be redirected to the login page...", + }) + loader.ReloadBackend() +} diff --git a/src/api/httpauth/login.go b/src/api/httpauth/login.go new file mode 100644 index 00000000..db69c2c8 --- /dev/null +++ b/src/api/httpauth/login.go @@ -0,0 +1,62 @@ +// handlers.go +package httpauth + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" +) + +// LoginHandler issues a JWT cookie +func LoginHandler(w http.ResponseWriter, r *http.Request) { + var creds security.UserCredentials + err := json.NewDecoder(r.Body).Decode(&creds) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid JSON"}) + return + } + + // Check credentials using security package + valid, err := security.ValidateCredentials(creds) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + if !valid { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized - Invalid credentials"}) + return + } + + // Generate JWT + tokenString, err := security.GenerateJWT(creds.Username) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + + // Set cookie + http.SetCookie(w, &http.Cookie{ + Name: "AuthToken", + Value: tokenString, + Expires: time.Now().Add(time.Duration(config.GetAuthTokenLifetime()) * time.Minute), + HttpOnly: true, + Secure: true, + Path: "/", + SameSite: http.SameSiteStrictMode, + }) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"token": tokenString}) +} diff --git a/src/api/httpauth/logout.go b/src/api/httpauth/logout.go new file mode 100644 index 00000000..e0764ae3 --- /dev/null +++ b/src/api/httpauth/logout.go @@ -0,0 +1,32 @@ +package httpauth + +import ( + "encoding/json" + "net/http" + "strings" + "time" +) + +func LogoutHandler(w http.ResponseWriter, r *http.Request) { + // Clear the cookie by setting it with an expired time + http.SetCookie(w, &http.Cookie{ + Name: "AuthToken", + Value: "", + Expires: time.Now().Add(-time.Hour), // Set to past time to expire immediately + HttpOnly: true, + Secure: true, + Path: "/", + SameSite: http.SameSiteStrictMode, + }) + accept := r.Header.Get("Accept") + if accept != "" && strings.Contains(accept, "text/html") { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + return + } + // For API requests, return success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "message": "Successfully logged out", + }) +} diff --git a/src/api/httpauth/register.go b/src/api/httpauth/register.go new file mode 100644 index 00000000..5a901248 --- /dev/null +++ b/src/api/httpauth/register.go @@ -0,0 +1,52 @@ +package httpauth + +import ( + "encoding/json" + "net/http" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" +) + +// RegisterUserHandler registers new users +func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { + + // Handle preflight OPTIONS requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + var creds security.UserCredentials + err := json.NewDecoder(r.Body).Decode(&creds) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid JSON"}) + return + } + + // Hash the password + hashedPassword, err := security.HashPassword(creds.Password) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + + // Initialize Users map if nil + if config.GetUsers() == nil { + config.SetUsers(make(map[string]string)) + } + + // Add or update the user + config.SetUsers(map[string]string{creds.Username: hashedPassword}) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{ + "message": "User registered successfully", + "username": creds.Username, + }) +} diff --git a/src/api/login.go b/src/api/login.go deleted file mode 100644 index 1423fb6a..00000000 --- a/src/api/login.go +++ /dev/null @@ -1,242 +0,0 @@ -// handlers.go -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/SteamServerUI/SteamServerUI/v7/src/config" - "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" - "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" - "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" - "github.com/SteamServerUI/SteamServerUI/v7/src/logger" -) - -var setupReminderCount = 0 // to limit the number of setup reminders shown to the user - -// LoginHandler issues a JWT cookie -func LoginHandler(w http.ResponseWriter, r *http.Request) { - var creds security.UserCredentials - err := json.NewDecoder(r.Body).Decode(&creds) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid JSON"}) - return - } - - // Check credentials using security package - valid, err := security.ValidateCredentials(creds) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) - return - } - if !valid { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized - Invalid credentials"}) - return - } - - // Generate JWT - tokenString, err := security.GenerateJWT(creds.Username) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) - return - } - - // Set cookie - http.SetCookie(w, &http.Cookie{ - Name: "AuthToken", - Value: tokenString, - Expires: time.Now().Add(time.Duration(config.GetAuthTokenLifetime()) * time.Minute), - HttpOnly: true, - Secure: true, - Path: "/", - SameSite: http.SameSiteStrictMode, - }) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"token": tokenString}) -} - -// AuthMiddleware protects routes with cookie-based JWT -func AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Log request details for debugging - //logger.Web.Debug("Request Path:" + r.URL.Path) //very spammy - - // Check for first-time setup redirect - if config.GetIsFirstTimeSetup() { - totalSetupReminderCount := 3 // Defines how often we redirect the users reqests to the setup page - if setupReminderCount < totalSetupReminderCount { - if r.URL.Path == "/" && (r.Referer() == "" || r.Referer() != "/setup") { - remainingReminderCount := totalSetupReminderCount - setupReminderCount - logger.Web.Warn("🔍Redirecting to setup page, you should really enable authentication...") - logger.Web.Warn(fmt.Sprintf("You will be remined %s more times.", strconv.Itoa(remainingReminderCount))) - http.Redirect(w, r, "/setup", http.StatusTemporaryRedirect) - setupReminderCount++ - return - } - } - } - - if !config.GetAuthEnabled() { - next.ServeHTTP(w, r) - return - } - - cookie, err := r.Cookie("AuthToken") - if err != nil { - // Browser redirect check - accept := r.Header.Get("Accept") - if accept != "" && strings.Contains(accept, "text/html") { - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) - return - } - // API response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized - No token"}) - return - } - - valid, err := security.ValidateJWT(cookie.Value) - if err != nil || !valid { - // Browser redirect check - accept := r.Header.Get("Accept") - if accept != "" && strings.Contains(accept, "text/html") { - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) - return - } - // API response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized - Invalid token"}) - logger.Security.Warn("Unauthorized Request - Invalid token") - return - } - - next.ServeHTTP(w, r) - }) -} - -func LogoutHandler(w http.ResponseWriter, r *http.Request) { - // Clear the cookie by setting it with an expired time - http.SetCookie(w, &http.Cookie{ - Name: "AuthToken", - Value: "", - Expires: time.Now().Add(-time.Hour), // Set to past time to expire immediately - HttpOnly: true, - Secure: true, - Path: "/", - SameSite: http.SameSiteStrictMode, - }) - accept := r.Header.Get("Accept") - if accept != "" && strings.Contains(accept, "text/html") { - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) - return - } - // For API requests, return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{ - "message": "Successfully logged out", - }) -} - -// RegisterUserHandler registers new users -func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { - - // Handle preflight OPTIONS requests - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - - var creds security.UserCredentials - err := json.NewDecoder(r.Body).Decode(&creds) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid JSON"}) - return - } - - // Hash the password - hashedPassword, err := security.HashPassword(creds.Password) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) - return - } - - // Initialize Users map if nil - if config.GetUsers() == nil { - config.SetUsers(make(map[string]string)) - } - - // Add or update the user - config.SetUsers(map[string]string{creds.Username: hashedPassword}) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(map[string]string{ - "message": "User registered successfully", - "username": creds.Username, - }) -} - -// SetupFinalizeHandler marks setup as complete -func SetupFinalizeHandler(w http.ResponseWriter, r *http.Request) { - - //check if users map is nil or empty - if len(config.GetUsers()) == 0 { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(map[string]string{"error": "No users registered - cannot finalize setup at this time. You should really enable authentication - or click 'Skip authentication'"}) - return - } - - // Load existing config to update it - newConfig, err := config.LoadConfig() - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to load config"}) - return - } - - // Mark setup as complete and enable auth - config.SetIsFirstTimeSetup(false) - isTrue := true - newConfig.AuthEnabled = &isTrue // Set the pointer to true - - // Save the updated config - err = configchanger.SaveConfig(newConfig) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to save config"}) - return - } - - logger.Web.Info("User Setup finalized successfully") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{ - "message": "Setup finalized successfully", - "restart_hint": "You will be redirected to the login page...", - }) - loader.ReloadBackend() -} diff --git a/src/api/middleware/authmiddleware.go b/src/api/middleware/authmiddleware.go new file mode 100644 index 00000000..144687d7 --- /dev/null +++ b/src/api/middleware/authmiddleware.go @@ -0,0 +1,76 @@ +package middleware + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" +) + +var setupReminderCount = 0 // to limit the number of setup reminders shown to the user + +// AuthMiddleware protects routes with cookie-based JWT +func AuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Log request details for debugging + //logger.Web.Debug("Request Path:" + r.URL.Path) //very spammy + + // Check for first-time setup redirect + if config.GetIsFirstTimeSetup() { + totalSetupReminderCount := 3 // Defines how often we redirect the users reqests to the setup page + if setupReminderCount < totalSetupReminderCount { + if r.URL.Path == "/" && (r.Referer() == "" || r.Referer() != "/setup") { + remainingReminderCount := totalSetupReminderCount - setupReminderCount + logger.Web.Warn("🔍Redirecting to setup page, you should really enable authentication...") + logger.Web.Warn(fmt.Sprintf("You will be remined %s more times.", strconv.Itoa(remainingReminderCount))) + http.Redirect(w, r, "/setup", http.StatusTemporaryRedirect) + setupReminderCount++ + return + } + } + } + + if !config.GetAuthEnabled() { + next.ServeHTTP(w, r) + return + } + + cookie, err := r.Cookie("AuthToken") + if err != nil { + // Browser redirect check + accept := r.Header.Get("Accept") + if accept != "" && strings.Contains(accept, "text/html") { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + return + } + // API response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized - No token"}) + return + } + + valid, err := security.ValidateJWT(cookie.Value) + if err != nil || !valid { + // Browser redirect check + accept := r.Header.Get("Accept") + if accept != "" && strings.Contains(accept, "text/html") { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + return + } + // API response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized - Invalid token"}) + logger.Security.Warn("Unauthorized Request - Invalid token") + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/src/api/routes.go b/src/api/routes.go index 1b718a1a..c29b0d50 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -4,6 +4,7 @@ import ( "io/fs" "net/http" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/httpauth" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" "github.com/SteamServerUI/SteamServerUI/v7/src/managers/backupmgr" @@ -19,8 +20,8 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { // Unprotected auth routes twoboxformAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/twoboxform") mux.Handle("/twoboxform/", http.StripPrefix("/twoboxform/", http.FileServer(http.FS(twoboxformAssetsFS)))) - mux.HandleFunc("/auth/login", LoginHandler) // Token issuer - mux.HandleFunc("/auth/logout", LogoutHandler) + mux.HandleFunc("/auth/login", httpauth.LoginHandler) // Token issuer + mux.HandleFunc("/auth/logout", httpauth.LogoutHandler) mux.HandleFunc("/login", ServeTwoBoxFormTemplate) // Protected routes (wrapped with middleware) @@ -72,13 +73,13 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/custom-detections/delete/", detectionmgr.HandleDeleteCustomDetection) // Authentication protectedMux.HandleFunc("/changeuser", ServeTwoBoxFormTemplate) - protectedMux.HandleFunc("/api/v2/auth/adduser", RegisterUserHandler) // user registration and change password + protectedMux.HandleFunc("/api/v2/auth/adduser", httpauth.RegisterUserHandler) // user registration and change password protectedMux.HandleFunc("/api/v2/auth/whoami", WhoAmIHandler) // Setup protectedMux.HandleFunc("/setup", ServeTwoBoxFormTemplate) - protectedMux.HandleFunc("/api/v2/auth/setup/register", RegisterUserHandler) // user registration - protectedMux.HandleFunc("/api/v2/auth/setup/finalize", SetupFinalizeHandler) + protectedMux.HandleFunc("/api/v2/auth/setup/register", httpauth.RegisterUserHandler) // user registration + protectedMux.HandleFunc("/api/v2/auth/setup/finalize", httpauth.ActivateAuthHandler) // SteamServerUI diff --git a/src/api/start.go b/src/api/start.go index 5c4b55e2..5074e1fb 100644 --- a/src/api/start.go +++ b/src/api/start.go @@ -7,6 +7,7 @@ import ( "net/http/pprof" "sync" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/middleware" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" @@ -26,7 +27,7 @@ func StartWebServer(wg *sync.WaitGroup) { mux, protectedMux := SetupRoutes() // Apply middleware only to protected routes - mux.Handle("/", AuthMiddleware(protectedMux)) // Wrap protected routes under root + mux.Handle("/", middleware.AuthMiddleware(protectedMux)) // Wrap protected routes under root httpLogger := log.New(&APIServerLogger{}, "", 0) // Start HTTP server From c5ff3f6e11ba7fe2e1abd57efd7edfb1775c3c87 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 14 Oct 2025 03:05:18 +0200 Subject: [PATCH 44/93] refactor: relocate API functions into subpages for better organization --- server.go | 4 +- src/api/handlers.go | 67 ++++++++ src/api/http.go | 144 ------------------ src/api/{ => httpauth}/whoami.go | 2 +- src/api/{start.go => httpstart.go} | 0 src/api/{ => legacyapi}/connectedplayers.go | 2 +- src/api/legacyapi/startstop.go | 41 +++++ src/api/{ => pages}/TwoBoxForm.go | 2 +- src/api/{ => pages}/configpage.go | 2 +- src/api/{ => pages}/detectionmanagerpage.go | 2 +- src/api/{ => pages}/indexpage.go | 2 +- src/api/pages/svelteui.go | 32 ++++ src/api/{ => pages}/templatevars.go | 2 +- src/api/routes.go | 72 +++++---- src/api/{ => runfileapi}/runfile.go | 7 +- src/api/{ => runfileapi}/runfilegallery.go | 10 +- src/api/{socket => socketapi}/examples.md | 0 src/api/{socket => socketapi}/socket-lin.go | 2 +- src/api/{socket => socketapi}/socket-win.go | 2 +- src/api/sscmapi/handlers.go | 68 +++++++++ src/api/{commands.go => sscmapi/sscm.go} | 2 +- src/api/{http-sse.go => sseapi/sse.go} | 2 +- src/api/svelteui.go | 59 ------- .../{systeminfo.go => sysinfoapi/sysinfo.go} | 2 +- 24 files changed, 267 insertions(+), 261 deletions(-) create mode 100644 src/api/handlers.go delete mode 100644 src/api/http.go rename src/api/{ => httpauth}/whoami.go (96%) rename src/api/{start.go => httpstart.go} (100%) rename src/api/{ => legacyapi}/connectedplayers.go (98%) create mode 100644 src/api/legacyapi/startstop.go rename src/api/{ => pages}/TwoBoxForm.go (99%) rename src/api/{ => pages}/configpage.go (99%) rename src/api/{ => pages}/detectionmanagerpage.go (98%) rename src/api/{ => pages}/indexpage.go (99%) create mode 100644 src/api/pages/svelteui.go rename src/api/{ => pages}/templatevars.go (99%) rename src/api/{ => runfileapi}/runfile.go (98%) rename src/api/{ => runfileapi}/runfilegallery.go (89%) rename src/api/{socket => socketapi}/examples.md (100%) rename src/api/{socket => socketapi}/socket-lin.go (98%) rename src/api/{socket => socketapi}/socket-win.go (98%) create mode 100644 src/api/sscmapi/handlers.go rename src/api/{commands.go => sscmapi/sscm.go} (99%) rename src/api/{http-sse.go => sseapi/sse.go} (99%) delete mode 100644 src/api/svelteui.go rename src/api/{systeminfo.go => sysinfoapi/sysinfo.go} (98%) diff --git a/server.go b/server.go index b9abcf5d..9f2dec7c 100644 --- a/server.go +++ b/server.go @@ -25,7 +25,7 @@ import ( "sync" "github.com/SteamServerUI/SteamServerUI/v7/src/api" - "github.com/SteamServerUI/SteamServerUI/v7/src/api/socket" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/socketapi" "github.com/SteamServerUI/SteamServerUI/v7/src/cli" "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" @@ -56,7 +56,7 @@ func main() { loader.AfterStartComplete(&wg) wg.Wait() logger.Main.Debug("Starting socket server...") - socket.StartSocketServer(&wg) + socketapi.StartSocketServer(&wg) logger.Main.Debug("Starting webserver...") api.StartWebServer(&wg) logger.Main.Debug("Initializing SSUICLI...") diff --git a/src/api/handlers.go b/src/api/handlers.go new file mode 100644 index 00000000..1928a109 --- /dev/null +++ b/src/api/handlers.go @@ -0,0 +1,67 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" +) + +func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { + runState := gamemgr.InternalIsServerRunning() + response := map[string]interface{}{ + "isRunning": runState, + "uuid": gamemgr.GameServerUUID.String(), + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, "Failed to respond with Game Server status", http.StatusInternalServerError) + return + } +} + +func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) { + + // Only allow GET requests + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) + return + } + + logger.Core.Info("Running SteamCMD") + _, err := steamcmd.InstallAndRunSteamCMD() + + // Update last execution time + + // Success: return 202 Accepted and JSON + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + if err == nil { + json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Success", "message": "SteamCMD ran successfully, gameserver files are up-to-date!"}) + return + } + // Failure: return 202 Accepted and JSON with the error message + json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Failed", "message": "SteamCMD ran unsuccessfully:" + err.Error()}) +} + +func HandleReloadBackend(w http.ResponseWriter, r *http.Request) { + logger.API.Debug("Received reloadbackend request from API") + // accept only GET requests + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) + return + } + // Reload all loaders + loader.ReloadBackend() + + // Set response headers and write JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + if err := json.NewEncoder(w).Encode(map[string]string{"status": "OK"}); err != nil { + http.Error(w, "Failed to write response", http.StatusInternalServerError) + return + } +} diff --git a/src/api/http.go b/src/api/http.go deleted file mode 100644 index cae38d09..00000000 --- a/src/api/http.go +++ /dev/null @@ -1,144 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/SteamServerUI/SteamServerUI/v7/src/config" - "github.com/SteamServerUI/SteamServerUI/v7/src/localization" - "github.com/SteamServerUI/SteamServerUI/v7/src/logger" - "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" - "github.com/SteamServerUI/SteamServerUI/v7/src/managers/detectionmgr" - "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" - "github.com/SteamServerUI/SteamServerUI/v7/src/steamcmd" -) - -// StartServer HTTP handler -func StartServer(w http.ResponseWriter, r *http.Request) { - logger.API.Debug("Received start request from API") - if err := gamemgr.InternalStartServer(); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - logger.API.Error("Error starting server: " + err.Error()) - return - } - fmt.Fprint(w, localization.GetString("BackendText_ServerStarted")) - logger.API.Info("Server started.") -} - -// StopServer HTTP handler -func StopServer(w http.ResponseWriter, r *http.Request) { - logger.API.Debug("Received stop request from API") - if err := gamemgr.InternalStopServer(); err != nil { - if err.Error() == "server not running" { - fmt.Fprint(w, localization.GetString("BackendText_ServerNotRunningOrAlreadyStopped")) - logger.API.Warn("Server not running or was already stopped") - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - logger.API.Error("Error stopping server: " + err.Error()) - return - } - detectionmgr.ClearPlayers(detectionmgr.GetDetector()) - fmt.Fprint(w, localization.GetString("BackendText_ServerStopped")) - logger.API.Info("Server stopped.") -} - -func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { - runState := gamemgr.InternalIsServerRunning() - response := map[string]interface{}{ - "isRunning": runState, - "uuid": gamemgr.GameServerUUID.String(), - } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - http.Error(w, "Failed to respond with Game Server status", http.StatusInternalServerError) - return - } -} - -// CommandHandler handles POST requests to execute commands via commandmgr. -// Expects a command in the request body. Returns 204 on success or error details. -func CommandHandler(w http.ResponseWriter, r *http.Request) { - // Allow only POST requests - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Read command from request body - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Failed to read request body", http.StatusBadRequest) - return - } - command := strings.TrimSpace(string(body)) - - // Validate command - if command == "" { - http.Error(w, "Command cannot be empty", http.StatusBadRequest) - return - } - - // Execute command via commandmgr - if err := commandmgr.WriteCommand(command); err != nil { - switch err { - case os.ErrNotExist: - http.Error(w, "Command file path not configured", http.StatusInternalServerError) - case os.ErrInvalid: - http.Error(w, "Invalid command", http.StatusBadRequest) - default: - http.Error(w, "Failed to write command: "+err.Error(), http.StatusInternalServerError) - } - return - } - - // Success: return 204 No Content - w.WriteHeader(http.StatusNoContent) -} - -func HandleIsSSCMEnabled(w http.ResponseWriter, r *http.Request) { - // Only allow GET requests - if r.Method != http.MethodGet { - http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) - return - } - - // Check if SSCM is enabled - if !config.GetIsSSCMEnabled() { - http.Error(w, "SSCM is disabled", http.StatusForbidden) - return - } - - // Success: return 200 OK - w.WriteHeader(http.StatusOK) -} - -// run SteamCMD from API, but only allow once every 5 minutes to "kinda" prevent concurrent executions although that woluldnt hurn. -// If the user has a 5mbit connection, I cannot help them anyways. -func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) { - - // Only allow GET requests - if r.Method != http.MethodGet { - http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) - return - } - - logger.Core.Info("Running SteamCMD") - _, err := steamcmd.InstallAndRunSteamCMD() - - // Update last execution time - - // Success: return 202 Accepted and JSON - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - if err == nil { - json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Success", "message": "SteamCMD ran successfully, gameserver files are up-to-date!"}) - return - } - // Failure: return 202 Accepted and JSON with the error message - json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Failed", "message": "SteamCMD ran unsuccessfully:" + err.Error()}) -} diff --git a/src/api/whoami.go b/src/api/httpauth/whoami.go similarity index 96% rename from src/api/whoami.go rename to src/api/httpauth/whoami.go index 8b83c285..47176321 100644 --- a/src/api/whoami.go +++ b/src/api/httpauth/whoami.go @@ -1,4 +1,4 @@ -package api +package httpauth import ( "encoding/json" diff --git a/src/api/start.go b/src/api/httpstart.go similarity index 100% rename from src/api/start.go rename to src/api/httpstart.go diff --git a/src/api/connectedplayers.go b/src/api/legacyapi/connectedplayers.go similarity index 98% rename from src/api/connectedplayers.go rename to src/api/legacyapi/connectedplayers.go index bcb69228..94ba8f07 100644 --- a/src/api/connectedplayers.go +++ b/src/api/legacyapi/connectedplayers.go @@ -1,4 +1,4 @@ -package api +package legacyapi import ( "encoding/json" diff --git a/src/api/legacyapi/startstop.go b/src/api/legacyapi/startstop.go new file mode 100644 index 00000000..62afd0a8 --- /dev/null +++ b/src/api/legacyapi/startstop.go @@ -0,0 +1,41 @@ +package legacyapi + +import ( + "fmt" + "net/http" + + "github.com/SteamServerUI/SteamServerUI/v7/src/localization" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/detectionmgr" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/gamemgr" +) + +// StartServer HTTP handler +func StartServer(w http.ResponseWriter, r *http.Request) { + logger.API.Debug("Received start request from API") + if err := gamemgr.InternalStartServer(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.API.Error("Error starting server: " + err.Error()) + return + } + fmt.Fprint(w, localization.GetString("BackendText_ServerStarted")) + logger.API.Info("Server started.") +} + +// StopServer HTTP handler +func StopServer(w http.ResponseWriter, r *http.Request) { + logger.API.Debug("Received stop request from API") + if err := gamemgr.InternalStopServer(); err != nil { + if err.Error() == "server not running" { + fmt.Fprint(w, localization.GetString("BackendText_ServerNotRunningOrAlreadyStopped")) + logger.API.Warn("Server not running or was already stopped") + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + logger.API.Error("Error stopping server: " + err.Error()) + return + } + detectionmgr.ClearPlayers(detectionmgr.GetDetector()) + fmt.Fprint(w, localization.GetString("BackendText_ServerStopped")) + logger.API.Info("Server stopped.") +} diff --git a/src/api/TwoBoxForm.go b/src/api/pages/TwoBoxForm.go similarity index 99% rename from src/api/TwoBoxForm.go rename to src/api/pages/TwoBoxForm.go index 86ff770e..d206acf1 100644 --- a/src/api/TwoBoxForm.go +++ b/src/api/pages/TwoBoxForm.go @@ -1,4 +1,4 @@ -package api +package pages import ( "io/fs" diff --git a/src/api/configpage.go b/src/api/pages/configpage.go similarity index 99% rename from src/api/configpage.go rename to src/api/pages/configpage.go index cef76a49..e6ebbb9f 100644 --- a/src/api/configpage.go +++ b/src/api/pages/configpage.go @@ -1,4 +1,4 @@ -package api +package pages import ( "fmt" diff --git a/src/api/detectionmanagerpage.go b/src/api/pages/detectionmanagerpage.go similarity index 98% rename from src/api/detectionmanagerpage.go rename to src/api/pages/detectionmanagerpage.go index 189658f5..01c59e0b 100644 --- a/src/api/detectionmanagerpage.go +++ b/src/api/pages/detectionmanagerpage.go @@ -1,4 +1,4 @@ -package api +package pages import ( "fmt" diff --git a/src/api/indexpage.go b/src/api/pages/indexpage.go similarity index 99% rename from src/api/indexpage.go rename to src/api/pages/indexpage.go index 305fb018..66b6ecb0 100644 --- a/src/api/indexpage.go +++ b/src/api/pages/indexpage.go @@ -1,4 +1,4 @@ -package api +package pages import ( "io/fs" diff --git a/src/api/pages/svelteui.go b/src/api/pages/svelteui.go new file mode 100644 index 00000000..240e7e2a --- /dev/null +++ b/src/api/pages/svelteui.go @@ -0,0 +1,32 @@ +package pages + +import ( + "io" + "io/fs" + "net/http" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" +) + +func ServeSvelteUI(w http.ResponseWriter, r *http.Request) { + htmlFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/v2") + if err != nil { + http.Error(w, "Error accessing Svelte UI: "+err.Error(), http.StatusInternalServerError) + return + } + + htmlFile, err := htmlFS.Open("index.html") + if err != nil { + http.Error(w, "Error reading Svelte UI: "+err.Error(), http.StatusInternalServerError) + return + } + defer htmlFile.Close() + + // Stream the file content to the response + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, err = io.Copy(w, htmlFile) + if err != nil { + http.Error(w, "Error writing Svelte UI: "+err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/src/api/templatevars.go b/src/api/pages/templatevars.go similarity index 99% rename from src/api/templatevars.go rename to src/api/pages/templatevars.go index 4e0e7c62..4cfe63b1 100644 --- a/src/api/templatevars.go +++ b/src/api/pages/templatevars.go @@ -1,4 +1,4 @@ -package api +package pages // TemplateData holds data to be passed to templates type IndexTemplateData struct { diff --git a/src/api/routes.go b/src/api/routes.go index c29b0d50..8821672d 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -5,6 +5,12 @@ import ( "net/http" "github.com/SteamServerUI/SteamServerUI/v7/src/api/httpauth" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/legacyapi" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/pages" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/runfileapi" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/sscmapi" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/sseapi" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/sysinfoapi" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" "github.com/SteamServerUI/SteamServerUI/v7/src/managers/backupmgr" @@ -22,7 +28,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { mux.Handle("/twoboxform/", http.StripPrefix("/twoboxform/", http.FileServer(http.FS(twoboxformAssetsFS)))) mux.HandleFunc("/auth/login", httpauth.LoginHandler) // Token issuer mux.HandleFunc("/auth/logout", httpauth.LogoutHandler) - mux.HandleFunc("/login", ServeTwoBoxFormTemplate) + mux.HandleFunc("/login", pages.ServeTwoBoxFormTemplate) // Protected routes (wrapped with middleware) protectedMux := http.NewServeMux() @@ -30,32 +36,32 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/assets") protectedMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(legacyAssetsFS)))) - protectedMux.HandleFunc("/config", ServeConfigPage) - protectedMux.HandleFunc("/detectionmanager", ServeDetectionManager) - protectedMux.HandleFunc("/", ServeIndex) + protectedMux.HandleFunc("/config", pages.ServeConfigPage) + protectedMux.HandleFunc("/detectionmanager", pages.ServeDetectionManager) + protectedMux.HandleFunc("/", pages.ServeIndex) // --- SVELTE UI --- - protectedMux.HandleFunc("/v2", ServeSvelteUI) + protectedMux.HandleFunc("/v2", pages.ServeSvelteUI) svelteAssetsFS, _ := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/v2/assets") protectedMux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(svelteAssetsFS)))) - protectedMux.HandleFunc("/api/v2/loader/reloadbackend", HandleReloadAll) + protectedMux.HandleFunc("/api/v2/loader/reloadbackend", HandleReloadBackend) // SSE routes - protectedMux.HandleFunc("/console", GetLogOutput) - protectedMux.HandleFunc("/events", GetEventOutput) - protectedMux.HandleFunc("/logs/debug", GetDebugLogOutput) - protectedMux.HandleFunc("/logs/info", GetInfoLogOutput) - protectedMux.HandleFunc("/logs/warn", GetWarnLogOutput) - protectedMux.HandleFunc("/logs/error", GetErrorLogOutput) - protectedMux.HandleFunc("/logs/backend", GetBackendLogOutput) + protectedMux.HandleFunc("/console", sseapi.GetLogOutput) + protectedMux.HandleFunc("/events", sseapi.GetEventOutput) + protectedMux.HandleFunc("/logs/debug", sseapi.GetDebugLogOutput) + protectedMux.HandleFunc("/logs/info", sseapi.GetInfoLogOutput) + protectedMux.HandleFunc("/logs/warn", sseapi.GetWarnLogOutput) + protectedMux.HandleFunc("/logs/error", sseapi.GetErrorLogOutput) + protectedMux.HandleFunc("/logs/backend", sseapi.GetBackendLogOutput) // Server Control - protectedMux.HandleFunc("/start", StartServer) - protectedMux.HandleFunc("/stop", StopServer) - protectedMux.HandleFunc("/api/v2/server/start", StartServer) - protectedMux.HandleFunc("/api/v2/server/stop", StopServer) + protectedMux.HandleFunc("/start", legacyapi.StartServer) + protectedMux.HandleFunc("/stop", legacyapi.StopServer) + protectedMux.HandleFunc("/api/v2/server/start", legacyapi.StartServer) // TODO: should return json & get their own functions + protectedMux.HandleFunc("/api/v2/server/stop", legacyapi.StopServer) // TODO: should return json & get their own functions protectedMux.HandleFunc("/api/v2/server/status", GetGameServerRunState) - protectedMux.HandleFunc("/api/v2/server/status/connectedplayers", HandleConnectedPlayersList) + protectedMux.HandleFunc("/api/v2/server/status/connectedplayers", legacyapi.HandleConnectedPlayersList) backupHandler := backupmgr.NewHTTPHandler(backupmgr.GlobalBackupManager) protectedMux.HandleFunc("/api/v2/backups", backupHandler.ListBackupsHandler) @@ -64,42 +70,42 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { // Configuration protectedMux.HandleFunc("/saveconfigasjson", configchanger.SaveConfigForm) // legacy, used on config page protectedMux.HandleFunc("/api/v2/saveconfig", configchanger.SaveConfigRestful) // used on twoboxform - protectedMux.HandleFunc("/api/v2/SSCM/run", HandleCommand) // Command execution via SSCM (needs to be enable, config.IsSSCMEnabled) - protectedMux.HandleFunc("/api/v2/SSCM/enabled", HandleIsSSCMEnabled) // Check if SSCM is enabled + protectedMux.HandleFunc("/api/v2/SSCM/run", sscmapi.HandleCommand) // Command execution via SSCM (needs to be enable, config.IsSSCMEnabled) + protectedMux.HandleFunc("/api/v2/SSCM/enabled", sscmapi.HandleIsSSCMEnabled) // Check if SSCM is enabled protectedMux.HandleFunc("/api/v2/steamcmd/run", HandleRunSteamCMD) // Run SteamCMD // Custom Detections protectedMux.HandleFunc("/api/v2/custom-detections", detectionmgr.HandleCustomDetection) protectedMux.HandleFunc("/api/v2/custom-detections/delete/", detectionmgr.HandleDeleteCustomDetection) // Authentication - protectedMux.HandleFunc("/changeuser", ServeTwoBoxFormTemplate) + protectedMux.HandleFunc("/changeuser", pages.ServeTwoBoxFormTemplate) protectedMux.HandleFunc("/api/v2/auth/adduser", httpauth.RegisterUserHandler) // user registration and change password - protectedMux.HandleFunc("/api/v2/auth/whoami", WhoAmIHandler) + protectedMux.HandleFunc("/api/v2/auth/whoami", httpauth.WhoAmIHandler) // Setup - protectedMux.HandleFunc("/setup", ServeTwoBoxFormTemplate) + protectedMux.HandleFunc("/setup", pages.ServeTwoBoxFormTemplate) protectedMux.HandleFunc("/api/v2/auth/setup/register", httpauth.RegisterUserHandler) // user registration protectedMux.HandleFunc("/api/v2/auth/setup/finalize", httpauth.ActivateAuthHandler) // SteamServerUI // --- RUNFILE --- - protectedMux.HandleFunc("/api/v2/runfile/groups", HandleRunfileGroups) - protectedMux.HandleFunc("/api/v2/runfile/args", HandleRunfileArgs) - protectedMux.HandleFunc("/api/v2/runfile/args/update", HandleRunfileArgUpdate) - protectedMux.HandleFunc("/api/v2/runfile", HandleRunfile) - protectedMux.HandleFunc("/api/v2/runfile/save", HandleRunfileSave) - protectedMux.HandleFunc("/api/v2/runfile/hardreset", HandleSetRunfileGame) + protectedMux.HandleFunc("/api/v2/runfile/groups", runfileapi.HandleRunfileGroups) + protectedMux.HandleFunc("/api/v2/runfile/args", runfileapi.HandleRunfileArgs) + protectedMux.HandleFunc("/api/v2/runfile/args/update", runfileapi.HandleRunfileArgUpdate) + protectedMux.HandleFunc("/api/v2/runfile", runfileapi.HandleRunfile) + protectedMux.HandleFunc("/api/v2/runfile/save", runfileapi.HandleRunfileSave) + protectedMux.HandleFunc("/api/v2/runfile/hardreset", runfileapi.HandleSetRunfileGame) // --- LOADER --- - protectedMux.HandleFunc("/api/v2/loader/reloadrunfile", HandleReloadRunfile) + protectedMux.HandleFunc("/api/v2/loader/reloadrunfile", runfileapi.HandleReloadRunfile) // --- SETTINGS --- protectedMux.HandleFunc("/api/v2/settings/save", settings.SaveSetting) protectedMux.HandleFunc("/api/v2/settings", settings.RetrieveSettings) // --- OS STATS --- - protectedMux.HandleFunc("/api/v2/osstats", HandleGetOsStats) + protectedMux.HandleFunc("/api/v2/osstats", sysinfoapi.HandleGetOsStats) // --- RUNFILE GALLERY --- - protectedMux.HandleFunc("/api/v2/gallery", galleryHandler) - protectedMux.HandleFunc("/api/v2/gallery/select", selectHandler) + protectedMux.HandleFunc("/api/v2/gallery", runfileapi.GalleryHandler) + protectedMux.HandleFunc("/api/v2/gallery/select", runfileapi.GallerySelectHandler) return mux, protectedMux } diff --git a/src/api/runfile.go b/src/api/runfileapi/runfile.go similarity index 98% rename from src/api/runfile.go rename to src/api/runfileapi/runfile.go index 5dfdc488..09bd1b83 100644 --- a/src/api/runfile.go +++ b/src/api/runfileapi/runfile.go @@ -1,4 +1,4 @@ -package api +package runfileapi import ( "encoding/json" @@ -232,9 +232,6 @@ func HandleRunfileSave(w http.ResponseWriter, r *http.Request) { // HandleSetRunfileGame reloads the runfile and restarts most of the server. It can also be used to reload the runfile from Disk as a hard reset. func HandleSetRunfileGame(w http.ResponseWriter, r *http.Request) { - reloadMu.Lock() - defer reloadMu.Unlock() - // Restrict to POST method if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -287,8 +284,6 @@ func HandleSetRunfileGame(w http.ResponseWriter, r *http.Request) { func HandleReloadRunfile(w http.ResponseWriter, r *http.Request) { logger.API.Debug("Received reloadrunfile request from API") - reloadMu.Lock() - defer reloadMu.Unlock() // accept only GET requests if r.Method != http.MethodGet { http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) diff --git a/src/api/runfilegallery.go b/src/api/runfileapi/runfilegallery.go similarity index 89% rename from src/api/runfilegallery.go rename to src/api/runfileapi/runfilegallery.go index 6145185d..d8a7fb49 100644 --- a/src/api/runfilegallery.go +++ b/src/api/runfileapi/runfilegallery.go @@ -1,4 +1,4 @@ -package api +package runfileapi import ( "encoding/json" @@ -16,8 +16,8 @@ type response struct { Error string `json:"error,omitempty"` } -// galleryHandler handles GET /api/v2/gallery -func galleryHandler(w http.ResponseWriter, r *http.Request) { +// GalleryHandler handles GET /api/v2/gallery +func GalleryHandler(w http.ResponseWriter, r *http.Request) { logger.Runfile.Info("Handling GET /api/v2/gallery request") forceUpdate := strings.ToLower(r.URL.Query().Get("forceUpdate")) == "true" @@ -32,8 +32,8 @@ func galleryHandler(w http.ResponseWriter, r *http.Request) { sendResponse(w, http.StatusOK, response{Data: runfiles}) } -// selectHandler handles POST /api/v2/gallery/select -func selectHandler(w http.ResponseWriter, r *http.Request) { +// GallerySelectHandler handles POST /api/v2/gallery/select +func GallerySelectHandler(w http.ResponseWriter, r *http.Request) { logger.Runfile.Info("Handling POST /api/v2/gallery/select request") var req struct { diff --git a/src/api/socket/examples.md b/src/api/socketapi/examples.md similarity index 100% rename from src/api/socket/examples.md rename to src/api/socketapi/examples.md diff --git a/src/api/socket/socket-lin.go b/src/api/socketapi/socket-lin.go similarity index 98% rename from src/api/socket/socket-lin.go rename to src/api/socketapi/socket-lin.go index 76ef5f8d..685a8933 100644 --- a/src/api/socket/socket-lin.go +++ b/src/api/socketapi/socket-lin.go @@ -1,7 +1,7 @@ //go:build linux // +build linux -package socket +package socketapi import ( "context" diff --git a/src/api/socket/socket-win.go b/src/api/socketapi/socket-win.go similarity index 98% rename from src/api/socket/socket-win.go rename to src/api/socketapi/socket-win.go index ceed2717..687a1525 100644 --- a/src/api/socket/socket-win.go +++ b/src/api/socketapi/socket-win.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package socket +package socketapi import ( "context" diff --git a/src/api/sscmapi/handlers.go b/src/api/sscmapi/handlers.go new file mode 100644 index 00000000..c69b1cfa --- /dev/null +++ b/src/api/sscmapi/handlers.go @@ -0,0 +1,68 @@ +package sscmapi + +import ( + "io" + "net/http" + "os" + "strings" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/managers/commandmgr" +) + +// CommandHandler handles POST requests to execute commands via commandmgr. +// Expects a command in the request body. Returns 204 on success or error details. +func CommandHandler(w http.ResponseWriter, r *http.Request) { + // Allow only POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Read command from request body + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusBadRequest) + return + } + command := strings.TrimSpace(string(body)) + + // Validate command + if command == "" { + http.Error(w, "Command cannot be empty", http.StatusBadRequest) + return + } + + // Execute command via commandmgr + if err := commandmgr.WriteCommand(command); err != nil { + switch err { + case os.ErrNotExist: + http.Error(w, "Command file path not configured", http.StatusInternalServerError) + case os.ErrInvalid: + http.Error(w, "Invalid command", http.StatusBadRequest) + default: + http.Error(w, "Failed to write command: "+err.Error(), http.StatusInternalServerError) + } + return + } + + // Success: return 204 No Content + w.WriteHeader(http.StatusNoContent) +} + +func HandleIsSSCMEnabled(w http.ResponseWriter, r *http.Request) { + // Only allow GET requests + if r.Method != http.MethodGet { + http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) + return + } + + // Check if SSCM is enabled + if !config.GetIsSSCMEnabled() { + http.Error(w, "SSCM is disabled", http.StatusForbidden) + return + } + + // Success: return 200 OK + w.WriteHeader(http.StatusOK) +} diff --git a/src/api/commands.go b/src/api/sscmapi/sscm.go similarity index 99% rename from src/api/commands.go rename to src/api/sscmapi/sscm.go index 99249861..28ed49c8 100644 --- a/src/api/commands.go +++ b/src/api/sscmapi/sscm.go @@ -1,4 +1,4 @@ -package api +package sscmapi import ( "encoding/json" diff --git a/src/api/http-sse.go b/src/api/sseapi/sse.go similarity index 99% rename from src/api/http-sse.go rename to src/api/sseapi/sse.go index 4d0010b3..d0533438 100644 --- a/src/api/http-sse.go +++ b/src/api/sseapi/sse.go @@ -1,4 +1,4 @@ -package api +package sseapi import ( "net/http" diff --git a/src/api/svelteui.go b/src/api/svelteui.go deleted file mode 100644 index 1bfd3631..00000000 --- a/src/api/svelteui.go +++ /dev/null @@ -1,59 +0,0 @@ -package api - -import ( - "encoding/json" - "io" - "io/fs" - "net/http" - "sync" - - "github.com/SteamServerUI/SteamServerUI/v7/src/config" - "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" - "github.com/SteamServerUI/SteamServerUI/v7/src/logger" -) - -var reloadMu sync.Mutex - -func ServeSvelteUI(w http.ResponseWriter, r *http.Request) { - htmlFS, err := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/v2") - if err != nil { - http.Error(w, "Error accessing Svelte UI: "+err.Error(), http.StatusInternalServerError) - return - } - - htmlFile, err := htmlFS.Open("index.html") - if err != nil { - http.Error(w, "Error reading Svelte UI: "+err.Error(), http.StatusInternalServerError) - return - } - defer htmlFile.Close() - - // Stream the file content to the response - w.Header().Set("Content-Type", "text/html; charset=utf-8") - _, err = io.Copy(w, htmlFile) - if err != nil { - http.Error(w, "Error writing Svelte UI: "+err.Error(), http.StatusInternalServerError) - return - } -} - -func HandleReloadAll(w http.ResponseWriter, r *http.Request) { - logger.API.Debug("Received reloadbackend request from API") - reloadMu.Lock() - defer reloadMu.Unlock() - // accept only GET requests - if r.Method != http.MethodGet { - http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) - return - } - // Reload all loaders - loader.ReloadBackend() - - // Set response headers and write JSON response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusAccepted) - if err := json.NewEncoder(w).Encode(map[string]string{"status": "OK"}); err != nil { - http.Error(w, "Failed to write response", http.StatusInternalServerError) - return - } -} diff --git a/src/api/systeminfo.go b/src/api/sysinfoapi/sysinfo.go similarity index 98% rename from src/api/systeminfo.go rename to src/api/sysinfoapi/sysinfo.go index a3386555..b4725059 100644 --- a/src/api/systeminfo.go +++ b/src/api/sysinfoapi/sysinfo.go @@ -1,4 +1,4 @@ -package api +package sysinfoapi import ( "encoding/json" From 4b0f06b4d63b12478e4b3dfd989d6c82aa3379b8 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 14 Oct 2025 03:13:24 +0200 Subject: [PATCH 45/93] refactor activateAuth (FinalizeSetup) to use setters instead of legacy configchanger --- src/api/httpauth/activate.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/api/httpauth/activate.go b/src/api/httpauth/activate.go index a807a603..1fbd0647 100644 --- a/src/api/httpauth/activate.go +++ b/src/api/httpauth/activate.go @@ -5,7 +5,6 @@ import ( "net/http" "github.com/SteamServerUI/SteamServerUI/v7/src/config" - "github.com/SteamServerUI/SteamServerUI/v7/src/config/configchanger" "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) @@ -21,29 +20,24 @@ func ActivateAuthHandler(w http.ResponseWriter, r *http.Request) { return } - // Load existing config to update it - newConfig, err := config.LoadConfig() + // Mark setup as complete and enable auth + err := config.SetIsFirstTimeSetup(false) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to load config"}) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to SetIsFirstTimeSetup in config"}) return } - - // Mark setup as complete and enable auth - config.SetIsFirstTimeSetup(false) - isTrue := true - newConfig.AuthEnabled = &isTrue // Set the pointer to true - - // Save the updated config - err = configchanger.SaveConfig(newConfig) + err = config.SetAuthEnabled(true) if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to save config"}) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error - Failed to SetAuthEnabled in config"}) return } + loader.ReloadBackend() + logger.Web.Info("User Setup finalized successfully") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -51,5 +45,4 @@ func ActivateAuthHandler(w http.ResponseWriter, r *http.Request) { "message": "Setup finalized successfully", "restart_hint": "You will be redirected to the login page...", }) - loader.ReloadBackend() } From 1b1a4656b7615378365abb1e1983a6aff67f9c68 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 14 Oct 2025 03:58:37 +0200 Subject: [PATCH 46/93] adds jwt based api keys (long-lived user tokens for now) --- src/api/httpauth/registerapikey.go | 102 ++++++++++++++++++ .../httpauth/{register.go => registeruser.go} | 8 ++ src/api/routes.go | 3 +- src/core/security/auth.go | 11 +- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/api/httpauth/registerapikey.go rename src/api/httpauth/{register.go => registeruser.go} (84%) diff --git a/src/api/httpauth/registerapikey.go b/src/api/httpauth/registerapikey.go new file mode 100644 index 00000000..7fcb1b0a --- /dev/null +++ b/src/api/httpauth/registerapikey.go @@ -0,0 +1,102 @@ +package httpauth + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/google/uuid" +) + +func RegisterAPIKeyHandler(w http.ResponseWriter, r *http.Request) { + + // Handle preflight OPTIONS requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + // Allow only GET or POST methods + if r.Method != http.MethodGet && r.Method != http.MethodPost { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "Method Not Allowed"}) + return + } + + // Set default duration for GET requests, require duration for POST + durationMonths := 1 + if r.Method == http.MethodPost { + var reqBody struct { + DurationMonths *int `json:"durationMonths"` // Use pointer to distinguish between 0 and unspecified + } + err := json.NewDecoder(r.Body).Decode(&reqBody) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid JSON"}) + return + } + if reqBody.DurationMonths == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - durationMonths is required for POST"}) + return + } + if *reqBody.DurationMonths <= 0 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Duration must be positive"}) + return + } + if *reqBody.DurationMonths > 120 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Duration must be less than 10 years"}) + return + } + durationMonths = *reqBody.DurationMonths + } + + var creds security.UserCredentials + + // Generate a random UUID as the username + creds.Username = "apikey-" + uuid.NewString() + + // Hash a random UUID as the password + hashedPassword, err := security.HashPassword(uuid.NewString()) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + + // Initialize Users map if nil + if config.GetUsers() == nil { + config.SetUsers(make(map[string]string)) + } + + // Add or update the user + config.SetUsers(map[string]string{creds.Username: hashedPassword}) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + apikey, err := security.GenerateJWT(creds.Username, durationMonths) + expires := time.Now().AddDate(0, durationMonths, 0) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Internal Server Error"}) + return + } + json.NewEncoder(w).Encode(map[string]string{ + "message": "APIKey registered successfully", + "apikey": apikey, + "expires": expires.Format(time.RFC3339), + }) + logger.Security.Infof("APIKey %s registered successfully. Expires: %s ", creds.Username, expires.Format(time.RFC3339)) +} diff --git a/src/api/httpauth/register.go b/src/api/httpauth/registeruser.go similarity index 84% rename from src/api/httpauth/register.go rename to src/api/httpauth/registeruser.go index 5a901248..fa2fe0d2 100644 --- a/src/api/httpauth/register.go +++ b/src/api/httpauth/registeruser.go @@ -3,6 +3,7 @@ package httpauth import ( "encoding/json" "net/http" + "strings" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" @@ -26,6 +27,13 @@ func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { return } + if strings.HasPrefix(creds.Username, "apikey-") { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "Bad Request - Invalid Username"}) + return + } + // Hash the password hashedPassword, err := security.HashPassword(creds.Password) if err != nil { diff --git a/src/api/routes.go b/src/api/routes.go index 8821672d..cba56359 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -79,7 +79,8 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/custom-detections/delete/", detectionmgr.HandleDeleteCustomDetection) // Authentication protectedMux.HandleFunc("/changeuser", pages.ServeTwoBoxFormTemplate) - protectedMux.HandleFunc("/api/v2/auth/adduser", httpauth.RegisterUserHandler) // user registration and change password + protectedMux.HandleFunc("/api/v2/auth/adduser", httpauth.RegisterUserHandler) // user registration and change password + protectedMux.HandleFunc("/api/v2/auth/setup/apikey", httpauth.RegisterAPIKeyHandler) // apikey registration and change password protectedMux.HandleFunc("/api/v2/auth/whoami", httpauth.WhoAmIHandler) // Setup diff --git a/src/core/security/auth.go b/src/core/security/auth.go index 0194fbef..3aeb57fa 100644 --- a/src/core/security/auth.go +++ b/src/core/security/auth.go @@ -4,6 +4,7 @@ package security //repurposed from a Jacksonthemaster private repo import ( + "strings" "time" "github.com/SteamServerUI/SteamServerUI/v7/src/config" @@ -19,8 +20,15 @@ type UserCredentials struct { } // GenerateJWT creates a JWT for a given username -func GenerateJWT(username string) (string, error) { +func GenerateJWT(username string, apikeyduration ...int) (string, error) { expirationTime := time.Now().Add(time.Duration(config.GetAuthTokenLifetime()) * time.Minute) + if strings.HasPrefix(username, "apikey-") { + durationMonths := 1 + if len(apikeyduration) > 0 { + durationMonths = apikeyduration[0] + } + expirationTime = time.Now().AddDate(0, durationMonths, 0) + } claims := &jwt.MapClaims{ "exp": expirationTime.Unix(), "iss": "StationeersServerUI", @@ -37,7 +45,6 @@ func GenerateJWT(username string) (string, error) { // ValidateCredentials checks username and password against stored users func ValidateCredentials(creds UserCredentials) (bool, error) { - // Placeholder: assumes config.Users is a map[string]string (username -> hashed password) storedHash, exists := config.GetUsers()[creds.Username] if !exists { return false, nil From 704f7c0d08666c755f00adad2beb6fac7cb3311e Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Wed, 15 Oct 2025 16:35:40 +0200 Subject: [PATCH 47/93] added a logger api that can log to logger.Plugin from an api request --- src/api/httpstart.go | 7 +++- src/api/pluginsapi/logs.go | 62 +++++++++++++++++++++++++++++++++ src/api/routes.go | 9 ++++- src/api/socketapi/socket-lin.go | 5 +-- src/api/socketapi/socket-win.go | 5 +-- src/logger/logger.go | 3 ++ 6 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 src/api/pluginsapi/logs.go diff --git a/src/api/httpstart.go b/src/api/httpstart.go index 5074e1fb..0a163c4d 100644 --- a/src/api/httpstart.go +++ b/src/api/httpstart.go @@ -24,7 +24,12 @@ func (cl *APIServerLogger) Write(p []byte) (n int, err error) { func StartWebServer(wg *sync.WaitGroup) { logger.Web.Info("Starting API services...") - mux, protectedMux := SetupRoutes() + mux, protectedMux := SetupAPIRoutes() + + // if debug mode, add socket only routes to http api for easy testing + if config.IsDebugMode { + SetupSocketAPIRoutes(protectedMux) + } // Apply middleware only to protected routes mux.Handle("/", middleware.AuthMiddleware(protectedMux)) // Wrap protected routes under root diff --git a/src/api/pluginsapi/logs.go b/src/api/pluginsapi/logs.go new file mode 100644 index 00000000..d3caea93 --- /dev/null +++ b/src/api/pluginsapi/logs.go @@ -0,0 +1,62 @@ +package pluginsapi + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" +) + +// a http handler that can be used to use logger.Plugin from the api +func PluginLogHandler(w http.ResponseWriter, r *http.Request) { + // Set response header to JSON + w.Header().Set("Content-Type", "application/json") + + // Only handle POST requests + if r.Method != http.MethodPost { + http.Error(w, `{"status":"error","message":"Method not allowed"}`, http.StatusMethodNotAllowed) + return + } + + // Define struct for incoming JSON + type logRequest struct { + Level string `json:"level"` + PluginName string `json:"pluginname"` + Message string `json:"message"` + } + + // Decode JSON request body + var req logRequest + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + http.Error(w, `{"status":"error","message":"Invalid JSON format"}`, http.StatusBadRequest) + return + } + + // Validate input + if req.Level == "" || req.PluginName == "" || req.Message == "" { + http.Error(w, `{"status":"error","message":"Missing required fields"}`, http.StatusBadRequest) + return + } + + msg := "<" + req.PluginName + "> " + req.Message + // Log based on level + switch strings.ToLower(req.Level) { + case "info": + logger.Plugin.Info(msg) + case "warn": + logger.Plugin.Warn(msg) + case "error": + logger.Plugin.Error(msg) + case "debug": + logger.Plugin.Debug(msg) + default: + http.Error(w, `{"status":"error","message":"Invalid log level"}`, http.StatusBadRequest) + return + } + + // Write success response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/src/api/routes.go b/src/api/routes.go index cba56359..26d912ae 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -7,6 +7,7 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/api/httpauth" "github.com/SteamServerUI/SteamServerUI/v7/src/api/legacyapi" "github.com/SteamServerUI/SteamServerUI/v7/src/api/pages" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/pluginsapi" "github.com/SteamServerUI/SteamServerUI/v7/src/api/runfileapi" "github.com/SteamServerUI/SteamServerUI/v7/src/api/sscmapi" "github.com/SteamServerUI/SteamServerUI/v7/src/api/sseapi" @@ -18,7 +19,8 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/settings" ) -func SetupRoutes() (*http.ServeMux, *http.ServeMux) { +// SetupAPIRoutes sets up API routes used by B O T H the web and socket servers +func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { // Set up handlers with auth middleware mux := http.NewServeMux() // Use a mux to apply middleware globally @@ -110,3 +112,8 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { return mux, protectedMux } + +// SetupSocketAPIRoutes adds routes that are E X C L U S I V E L Y available via sockets (if debug mode is enabled, these routes are added to the http api as well) +func SetupSocketAPIRoutes(APIMux *http.ServeMux) { + APIMux.HandleFunc("/api/v2/plugins/log", pluginsapi.PluginLogHandler) +} diff --git a/src/api/socketapi/socket-lin.go b/src/api/socketapi/socket-lin.go index 685a8933..72a32e29 100644 --- a/src/api/socketapi/socket-lin.go +++ b/src/api/socketapi/socket-lin.go @@ -26,8 +26,9 @@ func StartSocketServer(wg *sync.WaitGroup) { } // Set up routes - mux, protectedMux := api.SetupRoutes() - mux.Handle("/", protectedMux) + mux, APIMux := api.SetupAPIRoutes() + api.SetupSocketAPIRoutes(APIMux) + mux.Handle("/", APIMux) // Create Unix socket listener listener, err := net.Listen("unix", socketPath) diff --git a/src/api/socketapi/socket-win.go b/src/api/socketapi/socket-win.go index 687a1525..95598bc3 100644 --- a/src/api/socketapi/socket-win.go +++ b/src/api/socketapi/socket-win.go @@ -20,8 +20,9 @@ func StartSocketServer(wg *sync.WaitGroup) { logger.Socket.Info("Starting named pipe server...") // Set up routes - mux, protectedMux := api.SetupRoutes() - mux.Handle("/", protectedMux) + mux, httpAPIMux := api.SetupAPIRoutes() + api.SetupSocketAPIRoutes(httpAPIMux) + mux.Handle("/", httpAPIMux) // Create named pipe listener listener, err := winio.ListenPipe(pipePath, nil) diff --git a/src/logger/logger.go b/src/logger/logger.go index 1732d345..fa3af1e5 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -27,6 +27,7 @@ var ( Localization = &Logger{suffix: SYS_LOCALIZATION} Runfile = &Logger{suffix: SYS_RUNFILE} Socket = &Logger{suffix: SYS_SOCKET} + Plugin = &Logger{suffix: SYS_PLUGIN} ) // Severity Levels @@ -54,6 +55,7 @@ const ( SYS_LOCALIZATION = "LOCALIZATION" SYS_RUNFILE = "RUNFILE" SYS_SOCKET = "SOCKET" + SYS_PLUGIN = "PLUGIN" ) const ( @@ -81,6 +83,7 @@ var subsystemColors = map[string]string{ SYS_SECURITY: colorRed, // Screams "pay attention" SYS_LOCALIZATION: colorCyan, // Matches WEB, localization-related SYS_SOCKET: colorCyan, // Matches WEB, socket-related + SYS_PLUGIN: colorCyan, // Matches WEB, plugin-related } // Global channels and mutex for all loggers From 8e3d685471b74c54e0c6a03f35a19d461ef195c7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Wed, 15 Oct 2025 21:13:43 +0200 Subject: [PATCH 48/93] add GetArgValue method to RunFile for retrieving runtime values by flag --- src/steamserverui/runfile/args.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index d3c34898..be80ec86 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -145,6 +145,18 @@ func (rf *RunFile) getAllArgs() []GameArg { return allArgs } +func (rf *RunFile) GetArgValue(flag string) string { + for category := range rf.Args { + for i := range rf.Args[category] { + if rf.Args[category][i].Flag != flag { + continue + } + return rf.Args[category][i].RuntimeValue + } + } + return "" +} + // LoadRunfile loads the runfile and stores it in CurrentRunfile func LoadRunfile(gameName, runFilesFolder string) error { runfileMutex.Lock() From 45aa8959692a655b4b60b54187045891b38d910d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Wed, 15 Oct 2025 21:14:55 +0200 Subject: [PATCH 49/93] remove deprecated staioneers specific server configuration fields and related getters/setters in favor of runfiles --- SSUI/onboard_bundled/ui/config.html | 158 ----------------------- src/api/pages/TwoBoxForm.go | 18 +-- src/api/pages/configpage.go | 91 ------------- src/config/config.go | 143 +++------------------ src/config/getters.go | 152 ---------------------- src/config/helpers.go | 8 -- src/config/setters.go | 193 ---------------------------- src/config/vars.go | 47 ++----- src/core/loader/helpers.go | 25 +--- 9 files changed, 35 insertions(+), 800 deletions(-) diff --git a/SSUI/onboard_bundled/ui/config.html b/SSUI/onboard_bundled/ui/config.html index b080d61c..3bf8715c 100644 --- a/SSUI/onboard_bundled/ui/config.html +++ b/SSUI/onboard_bundled/ui/config.html @@ -53,11 +53,6 @@

{{.UIText_ServerConfig}}

- - {{if eq .IsNewTerrainAndSaveSystemTrueSelected "selected"}} - - {{end}} -
@@ -68,163 +63,10 @@

{{.UIText_PleaseSelectSection}}

{{.UIText_UseWizardAlternative}}

-
-

{{.UIText_BasicServerSettings}}

-
-
- - -
{{.UIText_ServerNameInfo}}
-
- -
- - -
{{.UIText_SaveNameInfo}}
-
- -
- - -
{{.UIText_WorldIDInfo}}
-
- -
- - -
{{.UIText_MaxPlayersInfo}}
-
- -
- - -
{{.UIText_ServerPasswordInfo}}
-
- - -
- - -
{{.UIText_ServerAuthSecretInfo}}
-
- -
- - -
{{.UIText_AutoSaveInfo}}
-
- -
- - -
{{.UIText_SaveIntervalInfo}}
-
- -
- - -
{{.UIText_AutoPauseServerInfo}}
-
-
-
- -
-

{{.UIText_NetworkConfiguration}}

-
-
- - -
{{.UIText_GamePortInfo}}
-
- -
- - -
{{.UIText_UpdatePortInfo}}
-
- -
- - -
{{.UIText_UPNPEnabledInfo}}
-
- -
- - -
{{.UIText_LocalIpAddressInfo}}
-
- -
- - -
{{.UIText_StartLocalHostInfo}}
-
- -
- - -
{{.UIText_ServerVisibleInfo}}
-
- -
- - -
{{.UIText_UseSteamP2PInfo}}
-
-
-
-

{{.UIText_AdvancedConfiguration}}

-
- - -
{{.UIText_AdminPasswordInfo}}
-
- -
- - -
{{.UIText_ServerExePathInfo}}
-
{{.UIText_ServerExePathInfo2}}
-
- -
- - -
{{.UIText_AdditionalParamsInfo}}
-
-
0 { - SaveName = parts[0] - fmt.Println("SaveName: " + SaveName) - } - if len(parts) > 1 { - WorldID = parts[1] - fmt.Println("WorldID: " + WorldID) - } - cfg.SaveInfo = "" - } - - if GameBranch != "public" && GameBranch != "beta" { - IsNewTerrainAndSaveSystem = false - } else { - IsNewTerrainAndSaveSystem = true - } + //if GameBranch != "public" && GameBranch != "beta" { + // IsNewTerrainAndSaveSystem = false + //} else { + // IsNewTerrainAndSaveSystem = true + //} // Set backup paths for old or new style saves - if IsNewTerrainAndSaveSystem { - // use new new style autosave folder - ConfiguredBackupDir = filepath.Join("./saves/", SaveName, "autosave") - } else { - // use old style Backups folder - ConfiguredBackupDir = filepath.Join("./saves/", SaveName, "Backup") - } - // use Safebackups folder either way. - ConfiguredSafeBackupDir = filepath.Join("./saves/", SaveName, "Safebackups") + //if IsNewTerrainAndSaveSystem { + // // use new new style autosave folder + // ConfiguredBackupDir = filepath.Join("./saves/", SaveName, "autosave") + //} else { + // // use old style Backups folder + // ConfiguredBackupDir = filepath.Join("./saves/", SaveName, "Backup") + //} + //// use Safebackups folder either way. + //ConfiguredSafeBackupDir = filepath.Join("./saves/", SaveName, "Safebackups") safeSaveConfig() } @@ -339,30 +249,7 @@ func safeSaveConfig() error { BackupKeepMonthlyFor: int(BackupKeepMonthlyFor / time.Hour), // Convert to hours BackupCleanupInterval: int(BackupCleanupInterval / time.Hour), // Convert to hours BackupWaitTime: int(BackupWaitTime / time.Second), // Convert to seconds - IsNewTerrainAndSaveSystem: &IsNewTerrainAndSaveSystem, GameBranch: GameBranch, - Difficulty: Difficulty, - StartCondition: StartCondition, - StartLocation: StartLocation, - ServerName: ServerName, - SaveName: SaveName, - WorldID: WorldID, - ServerMaxPlayers: ServerMaxPlayers, - ServerPassword: ServerPassword, - ServerAuthSecret: ServerAuthSecret, - AdminPassword: AdminPassword, - GamePort: GamePort, - UpdatePort: UpdatePort, - UPNPEnabled: &UPNPEnabled, - AutoSave: &AutoSave, - SaveInterval: SaveInterval, - AutoPauseServer: &AutoPauseServer, - LocalIpAddress: LocalIpAddress, - StartLocalHost: &StartLocalHost, - ServerVisible: &ServerVisible, - UseSteamP2P: &UseSteamP2P, - ExePath: ExePath, - AdditionalParams: AdditionalParams, Users: Users, AuthEnabled: &AuthEnabled, JwtKey: JwtKey, diff --git a/src/config/getters.go b/src/config/getters.go index ba951bea..0e7b93eb 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -109,158 +109,12 @@ func GetBackupCleanupInterval() time.Duration { return BackupCleanupInterval } -func GetIsNewTerrainAndSaveSystem() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return IsNewTerrainAndSaveSystem -} - func GetGameBranch() string { ConfigMu.RLock() defer ConfigMu.RUnlock() return GameBranch } -func GetDifficulty() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return Difficulty -} - -func GetStartCondition() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return StartCondition -} - -func GetStartLocation() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return StartLocation -} - -func GetServerName() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ServerName -} - -// special getter for backwards compatibility with SaveInfo -func GetLegacySaveInfo() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - saveinfo := SaveName + ";" + WorldID - return saveinfo -} - -func GetSaveName() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return SaveName -} - -func GetWorldID() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return WorldID -} - -func GetServerMaxPlayers() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ServerMaxPlayers -} - -func GetServerPassword() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ServerPassword -} - -func GetServerAuthSecret() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ServerAuthSecret -} - -func GetAdminPassword() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return AdminPassword -} - -func GetGamePort() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return GamePort -} - -func GetUpdatePort() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return UpdatePort -} - -func GetUPNPEnabled() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return UPNPEnabled -} - -func GetAutoSave() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return AutoSave -} - -func GetSaveInterval() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return SaveInterval -} - -func GetAutoPauseServer() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return AutoPauseServer -} - -func GetLocalIpAddress() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return LocalIpAddress -} - -func GetStartLocalHost() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return StartLocalHost -} - -func GetServerVisible() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ServerVisible -} - -func GetUseSteamP2P() bool { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return UseSteamP2P -} - -func GetExePath() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return ExePath -} - -func GetAdditionalParams() string { - ConfigMu.RLock() - defer ConfigMu.RUnlock() - return AdditionalParams -} - func GetUsers() map[string]string { ConfigMu.RLock() defer ConfigMu.RUnlock() @@ -526,12 +380,6 @@ func GetRunfilesFolder() string { return RunFilesFolder } -func GetIsStationeersMode() bool { - ConfigMu.Lock() - defer ConfigMu.Unlock() - return IsStationeersMode -} - // GetRunFilesFolder returns the RunFilesFolder func GetRunFilesFolder() string { ConfigMu.Lock() diff --git a/src/config/helpers.go b/src/config/helpers.go index 5aaa4d68..d283581d 100644 --- a/src/config/helpers.go +++ b/src/config/helpers.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "fmt" "os" - "runtime" "strconv" "strings" ) @@ -89,13 +88,6 @@ func getUsers(jsonValue map[string]string, envKey string, defaultValue map[strin return defaultValue } -func getDefaultExePath() string { - if runtime.GOOS == "windows" { - return "./rocketstation_DedicatedServer.exe" - } - return "./rocketstation_DedicatedServer.x86_64" -} - func generateJwtKey() string { // ensure we return JwtKey if it's set diff --git a/src/config/setters.go b/src/config/setters.go index 50a42b9b..0791fd9f 100644 --- a/src/config/setters.go +++ b/src/config/setters.go @@ -78,22 +78,6 @@ func SetNoSanityCheck(value bool) error { return nil } -func SetSaveName(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - SaveName = value - return nil -} - -func SetWorldID(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - WorldID = value - return nil -} - func SetUseRunfiles(value bool) error { ConfigMu.Lock() defer ConfigMu.Unlock() @@ -235,183 +219,6 @@ func SetGameBranch(value string) error { return safeSaveConfig() } -func SetDifficulty(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - Difficulty = value - return safeSaveConfig() -} - -func SetStartCondition(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - StartCondition = value - return safeSaveConfig() -} - -func SetStartLocation(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - StartLocation = value - return safeSaveConfig() -} - -func SetIsNewTerrainAndSaveSystem(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - IsNewTerrainAndSaveSystem = value - return safeSaveConfig() -} - -// Server Settings -func SetServerName(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ServerName = value - return safeSaveConfig() -} - -func SetSaveInfo(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - SaveInfo = value - return safeSaveConfig() -} - -func SetServerMaxPlayers(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ServerMaxPlayers = value - return safeSaveConfig() -} - -func SetServerPassword(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ServerPassword = value - return safeSaveConfig() -} - -func SetServerAuthSecret(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ServerAuthSecret = value - return safeSaveConfig() -} - -func SetAdminPassword(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - AdminPassword = value - return safeSaveConfig() -} - -func SetGamePort(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - GamePort = value - return safeSaveConfig() -} - -func SetUpdatePort(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - UpdatePort = value - return safeSaveConfig() -} - -func SetUPNPEnabled(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - UPNPEnabled = value - return safeSaveConfig() -} - -func SetAutoSave(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - AutoSave = value - return safeSaveConfig() -} - -func SetSaveInterval(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - SaveInterval = value - return safeSaveConfig() -} - -func SetAutoPauseServer(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - AutoPauseServer = value - return safeSaveConfig() -} - -func SetLocalIpAddress(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - LocalIpAddress = value - return safeSaveConfig() -} - -func SetStartLocalHost(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - StartLocalHost = value - return safeSaveConfig() -} - -func SetServerVisible(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ServerVisible = value - return safeSaveConfig() -} - -func SetUseSteamP2P(value bool) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - UseSteamP2P = value - return safeSaveConfig() -} - -func SetExePath(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - ExePath = value - return safeSaveConfig() -} - -func SetAdditionalParams(value string) error { - ConfigMu.Lock() - defer ConfigMu.Unlock() - - AdditionalParams = value - return safeSaveConfig() -} - func SetAutoStartServerOnStartup(value bool) error { ConfigMu.Lock() defer ConfigMu.Unlock() diff --git a/src/config/vars.go b/src/config/vars.go index e9bcb93b..0cee0dfe 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -17,32 +17,6 @@ if writes only happen via applyConfig or with ConfigMu locked. Uses getters wher var ConfigMu sync.RWMutex -// Game Server configuration -var ( - ServerName string - ServerMaxPlayers string - ServerPassword string - ServerAuthSecret string - AdminPassword string - GamePort string - UpdatePort string - LocalIpAddress string - ServerVisible bool - UseSteamP2P bool - AdditionalParams string - UPNPEnabled bool - StartLocalHost bool - SaveInfo string - SaveName string - WorldID string - SaveInterval string - AutoPauseServer bool - AutoSave bool - Difficulty string - StartCondition string - StartLocation string -) - // Logging, debugging and misc var ( IsDebugMode bool //only used for pprof server, keep it like this and check the log level instead. Debug = 10 @@ -52,7 +26,6 @@ var ( SSEMessageBufferSize = 2000 MaxSSEConnections = 20 GameServerAppID = "600760" - ExePath string GameBranch string SubsystemFilters []string AutoRestartServerTimer string @@ -67,7 +40,6 @@ var ( var ( UseRunfiles bool RunfileIdentifier string - IsStationeersMode bool ) // Runtime only variables @@ -99,16 +71,15 @@ var ( // Backup and cleanup settings var ( - IsCleanupEnabled bool - BackupKeepLastN int - BackupKeepDailyFor time.Duration - BackupKeepWeeklyFor time.Duration - BackupKeepMonthlyFor time.Duration - BackupCleanupInterval time.Duration - ConfiguredBackupDir string - ConfiguredSafeBackupDir string - BackupWaitTime time.Duration - IsNewTerrainAndSaveSystem bool + IsCleanupEnabled bool + BackupKeepLastN int + BackupKeepDailyFor time.Duration + BackupKeepWeeklyFor time.Duration + BackupKeepMonthlyFor time.Duration + BackupCleanupInterval time.Duration + ConfiguredBackupDir string + ConfiguredSafeBackupDir string + BackupWaitTime time.Duration ) // Authentication and security diff --git a/src/core/loader/helpers.go b/src/core/loader/helpers.go index 5cff29b7..4b7e1cc9 100644 --- a/src/core/loader/helpers.go +++ b/src/core/loader/helpers.go @@ -48,29 +48,8 @@ func PrintConfigDetails(logLevel ...string) { // Server Configuration server := map[string]string{ - "GameBranch": config.GetGameBranch(), - "ServerName": config.GetServerName(), - "WorldName": config.GetSaveName(), - "BackupWorldName": config.GetWorldID(), - "ServerMaxPlayers": config.GetServerMaxPlayers(), - "GamePort": config.GetGamePort(), - "UpdatePort": config.GetUpdatePort(), - "UPNPEnabled": fmt.Sprintf("%v", config.GetUPNPEnabled()), - "AutoSave": fmt.Sprintf("%v", config.GetAutoSave()), - "SaveInterval": config.GetSaveInterval(), - "AutoPauseServer": fmt.Sprintf("%v", config.GetAutoPauseServer()), - "LocalIpAddress": config.GetLocalIpAddress(), - "StartLocalHost": fmt.Sprintf("%v", config.GetStartLocalHost()), - "ServerVisible": fmt.Sprintf("%v", config.GetServerVisible()), - "UseSteamP2P": fmt.Sprintf("%v", config.GetUseSteamP2P()), - "ExePath": config.GetExePath(), - "AdditionalParams": config.GetAdditionalParams(), - "GameServerAppID": config.GetGameServerAppID(), - "Difficulty": config.GetDifficulty(), - "StartCondition": config.GetStartCondition(), - "StartLocation": config.GetStartLocation(), - "SaveInfo": config.GetLegacySaveInfo(), - "IsNewTerrainAndSaveSystem": fmt.Sprintf("%v", config.GetIsNewTerrainAndSaveSystem()), + "GameBranch": config.GetGameBranch(), + "GameServerAppID": config.GetGameServerAppID(), } printSection("Server Configuration", server) From 3933d983f7a65f3c1287b09c1fd0fef56b3be3e7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Wed, 15 Oct 2025 21:16:45 +0200 Subject: [PATCH 50/93] firstpass on making the BMv2 a standalone plugin by removing config references and replacing them with interim functions to not throw errors. BMv2 will be removed from the codebase and refactored into a plugin later. --- src/core/loader/loader.go | 8 +++----- src/managers/backupmgr/backupinterface.go | 7 ++++--- src/managers/backupmgr/manager.go | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index 4d1c15fe..e269ea2c 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -65,11 +65,9 @@ func ReloadBepInEx() { } func ReloadStationeersBackupManager() { - if config.GetIsStationeersMode() { - if err := backupmgr.ReloadBackupManagerFromConfig(); err != nil { - logger.Backup.Error("Failed to reload backup manager: " + err.Error()) - return - } + if err := backupmgr.ReloadBackupManagerFromConfig(); err != nil { + logger.Backup.Error("Failed to reload backup manager: " + err.Error()) + return } } diff --git a/src/managers/backupmgr/backupinterface.go b/src/managers/backupmgr/backupinterface.go index 1c3d55f0..a0879b2c 100644 --- a/src/managers/backupmgr/backupinterface.go +++ b/src/managers/backupmgr/backupinterface.go @@ -6,6 +6,7 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" "github.com/google/uuid" ) @@ -62,9 +63,9 @@ func GetBackupConfig() BackupConfig { id := uuid.New() bmIdentifier := "[BM" + id.String()[:6] + "]:" return BackupConfig{ - WorldName: config.GetSaveName(), - BackupDir: config.GetConfiguredBackupDir(), - SafeBackupDir: config.GetConfiguredSafeBackupDir(), + WorldName: runfile.CurrentRunfile.GetArgValue("SaveName"), + BackupDir: "./saves/" + runfile.CurrentRunfile.GetArgValue("SaveName") + "/autosave", + SafeBackupDir: "./saves/" + runfile.CurrentRunfile.GetArgValue("SaveName") + "/Safebackups", WaitTime: 30 * time.Second, // not sure why we are not using config.BackupWaitTime here, but ill not touch it in this commit (config rework) RetentionPolicy: RetentionPolicy{ KeepLastN: config.GetBackupKeepLastN(), diff --git a/src/managers/backupmgr/manager.go b/src/managers/backupmgr/manager.go index 5d61ce12..5ea5de20 100644 --- a/src/managers/backupmgr/manager.go +++ b/src/managers/backupmgr/manager.go @@ -148,7 +148,7 @@ func (m *BackupManager) handleNewBackup(filePath string) { time.Sleep(m.config.WaitTime) // save the world into Head save too if SSCM is enabled - if config.GetIsSSCMEnabled() && config.GetIsNewTerrainAndSaveSystem() { + if config.GetIsSSCMEnabled() { commandmgr.WriteCommand("SAVE") logger.Backup.Debug("HEAD Save triggered via SSCM") } else { From 7e17ef8558c0a66021e6f17d1b1300eafe3ad8c7 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Wed, 15 Oct 2025 21:46:23 +0200 Subject: [PATCH 51/93] changed default UI (/) to svelte UI wrapped old ui under /legacy updated svelte UI -> settings -> legacy views accordingly --- SSUI/onboard_bundled/assets/js/main.js | 2 -- .../detectionmanager/detectionmanager.html | 2 +- SSUI/onboard_bundled/ui/config.html | 6 +++--- SSUI/onboard_bundled/ui/index.html | 2 +- frontend/src/components/settings/ConfigManager.svelte | 6 +++--- .../src/components/settings/DetectionManager.svelte | 6 +++--- frontend/src/components/settings/SettingsView.svelte | 6 +++--- src/api/routes.go | 10 ++++++---- src/config/configchanger/changeconfig.go | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/SSUI/onboard_bundled/assets/js/main.js b/SSUI/onboard_bundled/assets/js/main.js index ab8acda6..469c853f 100644 --- a/SSUI/onboard_bundled/assets/js/main.js +++ b/SSUI/onboard_bundled/assets/js/main.js @@ -8,7 +8,6 @@ document.addEventListener('DOMContentLoaded', () => { resourceSaver(false); } typeh1(document.querySelector('h1'), 30); - if (window.location.pathname == '/') { setupTabs(); fetchDetectionEvents(); setupLogStreams({ @@ -34,7 +33,6 @@ document.addEventListener('DOMContentLoaded', () => { createPlanet(planetContainer, 70, 400, 28, 'rgba(200, 150, 200, 0.7)'); } console.warn("If you see errors for sscm.js or sscm.css, you may want to enable SSCM."); - } // Language flag selection const languageFlags = document.querySelectorAll('#language-flags img'); languageFlags.forEach(flag => { diff --git a/SSUI/onboard_bundled/detectionmanager/detectionmanager.html b/SSUI/onboard_bundled/detectionmanager/detectionmanager.html index e298d02e..f0f7158a 100644 --- a/SSUI/onboard_bundled/detectionmanager/detectionmanager.html +++ b/SSUI/onboard_bundled/detectionmanager/detectionmanager.html @@ -103,7 +103,7 @@

Regex Detection:

- +
diff --git a/SSUI/onboard_bundled/ui/config.html b/SSUI/onboard_bundled/ui/config.html index 3bf8715c..6c556b18 100644 --- a/SSUI/onboard_bundled/ui/config.html +++ b/SSUI/onboard_bundled/ui/config.html @@ -37,7 +37,7 @@

{{.UIText_ServerConfig}}

-
@@ -135,7 +135,7 @@

{{.UIText_TerrainSettingsHeader}}

- +
@@ -217,7 +217,7 @@

{{.UIText_ChannelConfiguration}}

{{.UIText_BannedPlayersListPathInfo}}
- +
diff --git a/SSUI/onboard_bundled/ui/index.html b/SSUI/onboard_bundled/ui/index.html index 4e70928e..3602b5c5 100644 --- a/SSUI/onboard_bundled/ui/index.html +++ b/SSUI/onboard_bundled/ui/index.html @@ -41,7 +41,7 @@

Stationeers Server UI v{{.Version}}{{.SSUIIdentifier}}

- +
diff --git a/frontend/src/components/settings/ConfigManager.svelte b/frontend/src/components/settings/ConfigManager.svelte index 699defb5..56c43777 100644 --- a/frontend/src/components/settings/ConfigManager.svelte +++ b/frontend/src/components/settings/ConfigManager.svelte @@ -11,7 +11,7 @@ const backendUrl = getCurrentBackendUrl(); // Construct the full URL with the folder query parameter - iframeSrc = `${backendUrl}/config`; + iframeSrc = `${backendUrl}/legacy/config`; } catch (error) { console.error('Error constructing iframe src:', error); } @@ -37,7 +37,7 @@ @@ -45,7 +45,7 @@ {#if loading}
-

Loading Config Manager...

+

Loading Legacy Config Manager...

{/if} diff --git a/frontend/src/components/settings/DetectionManager.svelte b/frontend/src/components/settings/DetectionManager.svelte index ef0d43bc..a32763ee 100644 --- a/frontend/src/components/settings/DetectionManager.svelte +++ b/frontend/src/components/settings/DetectionManager.svelte @@ -11,7 +11,7 @@ const backendUrl = getCurrentBackendUrl(); // Construct the full URL with the folder query parameter - iframeSrc = `${backendUrl}/detectionmanager`; + iframeSrc = `${backendUrl}/legacy/detectionmanager`; } catch (error) { console.error('Error constructing iframe src:', error); } @@ -37,7 +37,7 @@ @@ -45,7 +45,7 @@ {#if loading}
-

Loading Detection Manager...

+

Loading Legacy Detection Manager...

{/if} diff --git a/frontend/src/components/settings/SettingsView.svelte b/frontend/src/components/settings/SettingsView.svelte index a327f474..bc3ea5b9 100644 --- a/frontend/src/components/settings/SettingsView.svelte +++ b/frontend/src/components/settings/SettingsView.svelte @@ -26,8 +26,8 @@ class="settings-nav {activeSidebarTab === 'Backends' ? 'active' : ''}" onclick={() => selectSidebarTab('Backends')}>Backends + class="settings-nav {activeSidebarTab === 'Legacy Detection Manager' ? 'active' : ''}" + onclick={() => selectSidebarTab('Legacy Detection Manager')}>Legacy Detection Manager @@ -40,7 +40,7 @@ {:else if activeSidebarTab === 'Backends'} - {:else if activeSidebarTab === 'Detection Manager'} + {:else if activeSidebarTab === 'Legacy Detection Manager'} {:else if activeSidebarTab === 'Legacy Config Manager'} diff --git a/src/api/routes.go b/src/api/routes.go index 26d912ae..f0bf28f6 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -38,12 +38,14 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/assets") protectedMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(legacyAssetsFS)))) - protectedMux.HandleFunc("/config", pages.ServeConfigPage) - protectedMux.HandleFunc("/detectionmanager", pages.ServeDetectionManager) - protectedMux.HandleFunc("/", pages.ServeIndex) + protectedMux.HandleFunc("/legacy/config", pages.ServeConfigPage) + protectedMux.HandleFunc("/legacy/detectionmanager", pages.ServeDetectionManager) + + // Index page(s) + protectedMux.HandleFunc("/legacy", pages.ServeIndex) + protectedMux.HandleFunc("/", pages.ServeSvelteUI) // --- SVELTE UI --- - protectedMux.HandleFunc("/v2", pages.ServeSvelteUI) svelteAssetsFS, _ := fs.Sub(config.V1UIFS, "SSUI/onboard_bundled/v2/assets") protectedMux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(svelteAssetsFS)))) protectedMux.HandleFunc("/api/v2/loader/reloadbackend", HandleReloadBackend) diff --git a/src/config/configchanger/changeconfig.go b/src/config/configchanger/changeconfig.go index 073d7b40..6e7e6b2f 100644 --- a/src/config/configchanger/changeconfig.go +++ b/src/config/configchanger/changeconfig.go @@ -68,7 +68,7 @@ func SaveConfigForm(w http.ResponseWriter, r *http.Request) { return } - http.Redirect(w, r, "/config", http.StatusSeeOther) + http.Redirect(w, r, "/legacy/config", http.StatusSeeOther) } func SaveConfigRestful(w http.ResponseWriter, r *http.Request) { From a4cfa6e5d115d2e5ad5489e39b749b87b7d9d024 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Thu, 16 Oct 2025 01:42:00 +0200 Subject: [PATCH 52/93] disabled BMv2 and removed the IsNewTerrain setting --- src/config/config.go | 21 ++++++++++----------- src/core/loader/loader.go | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/config/config.go b/src/config/config.go index 07c3a69d..8722f210 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -40,17 +40,16 @@ type JsonConfig struct { AuthTokenLifetime int `json:"AuthTokenLifetime"` // SSUI Settings - IsNewTerrainAndSaveSystem *bool `json:"IsNewTerrainAndSaveSystem"` // Use new terrain and save system - LogClutterToConsole *bool `json:"LogClutterToConsole"` - IsSSCMEnabled *bool `json:"IsSSCMEnabled"` - IsBepInExEnabled *bool `json:"IsBepInExEnabled"` - AutoRestartServerTimer string `json:"AutoRestartServerTimer"` - IsConsoleEnabled *bool `json:"IsConsoleEnabled"` - LanguageSetting string `json:"LanguageSetting"` - AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` - SSUIIdentifier string `json:"SSUIIdentifier"` - SSUIWebPort string `json:"SSUIWebPort"` - UseRunfiles *bool `json:"UseRunfiles"` + LogClutterToConsole *bool `json:"LogClutterToConsole"` + IsSSCMEnabled *bool `json:"IsSSCMEnabled"` + IsBepInExEnabled *bool `json:"IsBepInExEnabled"` + AutoRestartServerTimer string `json:"AutoRestartServerTimer"` + IsConsoleEnabled *bool `json:"IsConsoleEnabled"` + LanguageSetting string `json:"LanguageSetting"` + AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"` + SSUIIdentifier string `json:"SSUIIdentifier"` + SSUIWebPort string `json:"SSUIWebPort"` + UseRunfiles *bool `json:"UseRunfiles"` // Update Settings IsUpdateEnabled *bool `json:"IsUpdateEnabled"` diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index e269ea2c..dd4b6f40 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -25,7 +25,7 @@ func InitBackend(wg *sync.WaitGroup) { ReloadConfig() ReloadRunfile() ReloadBepInEx() - ReloadStationeersBackupManager() + //ReloadStationeersBackupManager() ReloadLocalizer() ReloadAppInfoPoller() ReloadDiscordBot() @@ -38,7 +38,7 @@ func ReloadBackend() { logger.Core.Info("Reloading backend...") ReloadConfig() ReloadBepInEx() - ReloadStationeersBackupManager() + //ReloadStationeersBackupManager() ReloadLocalizer() ReloadAppInfoPoller() PrintConfigDetails() From 6e205d4813a53f2709517a298a30f72a7f41da1b Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Thu, 16 Oct 2025 03:50:37 +0200 Subject: [PATCH 53/93] updated devcontainer to expose socket api file, improve socket api --- .devcontainer/Dockerfile | 2 ++ .devcontainer/devcontainer.json | 3 +++ src/api/socketapi/examples.md | 8 ++++---- src/api/socketapi/socket-lin.go | 9 ++++++++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9681386c..c87c9209 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,5 +28,7 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ npm install -g npm@latest # Set up non-root user and workspace + +RUN mkdir -p /tmp/ssui && chmod 777 /tmp/ssui USER vscode WORKDIR /workspaces \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0346bdee..d3b18136 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,9 @@ }, "workspaceFolder": "/workspaces/project", "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/project,type=bind,consistency=cached", + "mounts": [ + "source=ssui-socket-volume,target=/tmp/ssui,type=volume" + ], "features": { "ghcr.io/devcontainers/features/go:1": { "version": "1.25.0" diff --git a/src/api/socketapi/examples.md b/src/api/socketapi/examples.md index bccf7cbe..a0e485cb 100644 --- a/src/api/socketapi/examples.md +++ b/src/api/socketapi/examples.md @@ -5,7 +5,7 @@ This guide explains how to interact with the socket-based `SSUI-API`, which expo ## Overview The socket server reuses the HTTP routes defined in `routes.go` but serves them over: -- **Linux**: A Unix socket at `/tmp/ssui.sock`. +- **Linux**: A Unix socket at `/tmp/ssui/ssui.sock`. - **Windows**: A named pipe at `\\.\pipe\ssui`. This is similar to how Docker uses `/var/run/docker.sock` for local API access. @@ -27,7 +27,7 @@ This is similar to how Docker uses `/var/run/docker.sock` for local API access. Use `curl` with the `--unix-socket` flag to send HTTP requests to the socket. Example for the `/api/v2/settings` endpoint: ```bash -curl --unix-socket /tmp/ssui.sock http://localhost/api/v2/settings +curl --unix-socket /tmp/ssui/ssui.sock http://localhost/api/v2/settings ``` **Expected Output**: JSON response from the `settings.RetrieveSettings` handler, e.g.: @@ -110,14 +110,14 @@ All routes from `routes.go` (e.g., `/api/v2/server/start`, `/api/v2/backups`) ar - **Linux**: ```bash - curl --unix-socket /tmp/ssui.sock http://localhost/api/v2/backups + curl --unix-socket /tmp/ssui/ssui.sock http://localhost/api/v2/backups ``` - **Windows**: Edit `$endpoint` in `test_namedpipe.ps1`, e.g., `$endpoint = "/api/v2/backups"`. For POST requests (e.g., `/api/v2/server/start`), add a JSON payload: - **Linux**: ```bash - curl --unix-socket /tmp/ssui.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start + curl --unix-socket /tmp/ssui/ssui.sock -X POST -H "Content-Type: application/json" -d '{"action":"start"}' http://localhost/api/v2/server/start ``` - **Windows**: Update the PowerShell script’s `$request`: ```powershell diff --git a/src/api/socketapi/socket-lin.go b/src/api/socketapi/socket-lin.go index 72a32e29..52a696cf 100644 --- a/src/api/socketapi/socket-lin.go +++ b/src/api/socketapi/socket-lin.go @@ -9,13 +9,14 @@ import ( "net" "net/http" "os" + "path/filepath" "sync" "github.com/SteamServerUI/SteamServerUI/v7/src/api" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) -const socketPath = "/tmp/ssui.sock" +const socketPath = "/tmp/ssui/ssui.sock" func StartSocketServer(wg *sync.WaitGroup) { logger.Socket.Info("Starting Unix socket server...") @@ -30,6 +31,12 @@ func StartSocketServer(wg *sync.WaitGroup) { api.SetupSocketAPIRoutes(APIMux) mux.Handle("/", APIMux) + // Create parent directory for the socket + parentDir := filepath.Dir(socketPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + logger.Socket.Error("Error creating parent directory: " + err.Error()) + return + } // Create Unix socket listener listener, err := net.Listen("unix", socketPath) if err != nil { From 17c85311381e72684d2fce8dd73be195ba4be8f2 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 17 Oct 2025 18:48:44 +0200 Subject: [PATCH 54/93] fix runfile loading & improve logging noise --- src/api/handlers.go | 2 -- src/api/pages/TwoBoxForm.go | 15 +++++++++------ src/api/runfileapi/runfile.go | 3 --- src/api/runfileapi/runfilegallery.go | 8 ++++---- src/core/loader/loader.go | 1 + src/core/loader/runfile.go | 4 ++-- src/steamcmd/steamcmd.go | 5 +++++ src/steamserverui/gallery/gallery.go | 19 +++++++++++++++---- src/steamserverui/runfile/args.go | 4 ++-- 9 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/api/handlers.go b/src/api/handlers.go index 1928a109..8ac5fc98 100644 --- a/src/api/handlers.go +++ b/src/api/handlers.go @@ -30,8 +30,6 @@ func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) { http.Error(w, "Only GET requests are allowed", http.StatusMethodNotAllowed) return } - - logger.Core.Info("Running SteamCMD") _, err := steamcmd.InstallAndRunSteamCMD() // Update last execution time diff --git a/src/api/pages/TwoBoxForm.go b/src/api/pages/TwoBoxForm.go index 5dd1a495..f560213a 100644 --- a/src/api/pages/TwoBoxForm.go +++ b/src/api/pages/TwoBoxForm.go @@ -129,7 +129,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { SecondaryLabelType: "hidden", SubmitButtonText: localization.GetString("UIText_PlsRead_SubmitButton"), SkipButtonText: localization.GetString("UIText_PlsRead_SkipButton"), - NextStep: "game_branch", + NextStep: "admin_account", }, "game_branch": { ID: "game_branch", @@ -364,11 +364,14 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) { data.Step = "welcome" } stepOrder := []string{ - "welcome", "pls_read", "game_branch", "server_name", "save_name", "world_id", "max_players", - "server_password", "discord_enabled", "discord_token", "control_panel_channel", "save_channel", - "log_channel", "connection_list_channel", "status_channel", "control_channel", - "network_config_choice", "game_port", "update_port", "upnp_enabled", - "local_ip_address", "admin_account", "finalize", + "welcome", "pls_read", + //"game_branch", + //"server_name", "save_name", "world_id", "max_players", + //"server_password", "discord_enabled", "discord_token", "control_panel_channel", "save_channel", + //"log_channel", "connection_list_channel", "status_channel", "control_channel", + //"network_config_choice", "game_port", "update_port", "upnp_enabled", + //"local_ip_address", + "admin_account", "finalize", } var stepSlice []Step for _, id := range stepOrder { diff --git a/src/api/runfileapi/runfile.go b/src/api/runfileapi/runfile.go index 09bd1b83..968b7d17 100644 --- a/src/api/runfileapi/runfile.go +++ b/src/api/runfileapi/runfile.go @@ -112,7 +112,6 @@ func HandleRunfileGroups(w http.ResponseWriter, r *http.Request) { } groups := runfile.GetUIGroups() - logger.Runfile.Info("fetched UI groups") writeJSONResponse(w, http.StatusOK, groups, "") } @@ -153,8 +152,6 @@ func HandleRunfileArgs(w http.ResponseWriter, r *http.Request) { for i, arg := range args { apiArgs[i] = toAPIGameArg(arg) } - - logger.Runfile.Info(fmt.Sprintf("fetched args for group=%s", group)) writeJSONResponse(w, http.StatusOK, apiArgs, "") } diff --git a/src/api/runfileapi/runfilegallery.go b/src/api/runfileapi/runfilegallery.go index d8a7fb49..82fdbf88 100644 --- a/src/api/runfileapi/runfilegallery.go +++ b/src/api/runfileapi/runfilegallery.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/gallery" ) @@ -18,7 +19,6 @@ type response struct { // GalleryHandler handles GET /api/v2/gallery func GalleryHandler(w http.ResponseWriter, r *http.Request) { - logger.Runfile.Info("Handling GET /api/v2/gallery request") forceUpdate := strings.ToLower(r.URL.Query().Get("forceUpdate")) == "true" runfiles, err := gallery.GetRunfileGallery(forceUpdate) @@ -28,13 +28,12 @@ func GalleryHandler(w http.ResponseWriter, r *http.Request) { return } - logger.Runfile.Info("Returning " + strconv.Itoa(len(runfiles)) + " runfiles from gallery") + logger.Runfile.Debug("Returning " + strconv.Itoa(len(runfiles)) + " runfiles from gallery") sendResponse(w, http.StatusOK, response{Data: runfiles}) } // GallerySelectHandler handles POST /api/v2/gallery/select func GallerySelectHandler(w http.ResponseWriter, r *http.Request) { - logger.Runfile.Info("Handling POST /api/v2/gallery/select request") var req struct { Identifier string `json:"identifier"` @@ -57,7 +56,8 @@ func GallerySelectHandler(w http.ResponseWriter, r *http.Request) { return } - logger.Runfile.Info("Successfully saved runfile " + req.Identifier) + logger.Runfile.Debug("Successfully saved runfile " + req.Identifier) + loader.ReloadBackend() sendResponse(w, http.StatusOK, response{Data: "Runfile " + req.Identifier + " saved"}) } diff --git a/src/core/loader/loader.go b/src/core/loader/loader.go index dd4b6f40..19cc7e6f 100644 --- a/src/core/loader/loader.go +++ b/src/core/loader/loader.go @@ -37,6 +37,7 @@ func ReloadBackend() { logger.Core.Info("Reloading backend...") ReloadConfig() + ReloadRunfile() ReloadBepInEx() //ReloadStationeersBackupManager() ReloadLocalizer() diff --git a/src/core/loader/runfile.go b/src/core/loader/runfile.go index bf16600e..648727ac 100644 --- a/src/core/loader/runfile.go +++ b/src/core/loader/runfile.go @@ -19,8 +19,8 @@ func InitRunfile(game string) error { return fmt.Errorf("game cannot be empty") } - logger.Runfile.Info("Updating runfile game to " + game) - logger.Runfile.Info("Stopping server if running") + logger.Runfile.Debug("Updating runfile game to " + game) + logger.Runfile.Debug("Stopping server if running") gamemgr.InternalStopServer() config.SetRunfileIdentifier(game) diff --git a/src/steamcmd/steamcmd.go b/src/steamcmd/steamcmd.go index be56777e..48417bf2 100644 --- a/src/steamcmd/steamcmd.go +++ b/src/steamcmd/steamcmd.go @@ -122,6 +122,11 @@ func runSteamCMD(steamCMDDir string) (int, error) { cmd.Env = newEnv } + if config.GetSkipSteamCMD() { + logger.Install.Warn("Skipping SteamCMD installation") + return 0, nil + } + // Run the command if config.GetLogLevel() == 10 { cmdString := strings.Join(cmd.Args, " ") diff --git a/src/steamserverui/gallery/gallery.go b/src/steamserverui/gallery/gallery.go index 2ddac3bc..5fa06626 100644 --- a/src/steamserverui/gallery/gallery.go +++ b/src/steamserverui/gallery/gallery.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/SteamServerUI/SteamServerUI/v7/src/config" + "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) @@ -44,7 +45,7 @@ func GetRunfileGallery(forceUpdate bool) ([]GalleryRunfile, error) { // Fetch manifest from GitHub Pages const manifestURL = "https://steamserverui.github.io/runfiles/manifest.ssui" - logger.Runfile.Info("Fetching runfile gallery from " + manifestURL) + logger.Runfile.Debug("Fetching runfile gallery from " + manifestURL) resp, err := http.Get(manifestURL) if err != nil { logger.Runfile.Error(fmt.Sprintf("Failed to fetch manifest: %v", err)) @@ -92,7 +93,7 @@ func SaveRunfileToDisk(identifier string) error { baseURL := "https://steamserverui.github.io/runfiles" fileURL := fmt.Sprintf("%s/%s", baseURL, filename) - logger.Runfile.Info("Fetching runfile from " + fileURL) + logger.Runfile.Debug("Fetching runfile from " + fileURL) resp, err := http.Get(fileURL) if err != nil { logger.Runfile.Error(fmt.Sprintf("Failed to fetch runfile %s: %v", filename, err)) @@ -106,7 +107,16 @@ func SaveRunfileToDisk(identifier string) error { } saveFilePath := filepath.Join(config.GetRunfilesFolder(), filename) - logger.Runfile.Info("Saving runfile to " + saveFilePath) + logger.Runfile.Debug("Saving runfile to " + saveFilePath) + + // get the dir of the saveFilePath, and os.MkdirAll it if it doesn't exist + dir := filepath.Dir(saveFilePath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0755); err != nil { + logger.Runfile.Error(fmt.Sprintf("Failed to create runfiles directory %s: %v", dir, err)) + return fmt.Errorf("couldn't create directory") + } + } // Create or overwrite the file file, err := os.Create(saveFilePath) @@ -122,7 +132,8 @@ func SaveRunfileToDisk(identifier string) error { return fmt.Errorf("couldn't save %s, disk's being dramatic", filename) } - logger.Runfile.Info("Successfully saved runfile " + filename) + logger.Runfile.Debug("Successfully saved runfile " + filename) + loader.InitRunfile(identifier) return nil } diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index be80ec86..d1f19bab 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -222,7 +222,7 @@ func LoadRunfile(gameName, runFilesFolder string) error { } CurrentRunfile = &runfile - logger.Runfile.Info(fmt.Sprintf("runfile loaded: path=%s", filePath)) + logger.Runfile.Debug(fmt.Sprintf("runfile loaded: path=%s", filePath)) return nil } @@ -276,7 +276,7 @@ func SaveRunfile() error { break } - logger.Runfile.Info(fmt.Sprintf("runfile saved: path=%s", filePath)) + logger.Runfile.Debug(fmt.Sprintf("runfile saved: path=%s", filePath)) return nil } From b79ad6846f45c855668e484c7344982231481b68 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 17 Oct 2025 19:13:43 +0200 Subject: [PATCH 55/93] added small logsCard to main dashboard --- .../components/Dashboard/DashboardView.svelte | 3 ++ .../Dashboard/cards/LogsCard.svelte | 39 +++++++++++++++++++ frontend/src/components/views/LogsView.svelte | 3 ++ 3 files changed, 45 insertions(+) create mode 100644 frontend/src/components/Dashboard/cards/LogsCard.svelte diff --git a/frontend/src/components/Dashboard/DashboardView.svelte b/frontend/src/components/Dashboard/DashboardView.svelte index 0dd620fb..2fe84435 100644 --- a/frontend/src/components/Dashboard/DashboardView.svelte +++ b/frontend/src/components/Dashboard/DashboardView.svelte @@ -3,12 +3,14 @@ import ConsoleCard from './cards/ConsoleCard.svelte'; import WarnCard from './cards/WarnCard.svelte'; import SystemInfoCard from './cards/SystemInfoCard.svelte'; + import LogsCard from './cards/LogsCard.svelte';
+
@@ -17,6 +19,7 @@ .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-auto-rows: minmax(0, 400px); gap: 1.5rem; padding: 1rem; } diff --git a/frontend/src/components/Dashboard/cards/LogsCard.svelte b/frontend/src/components/Dashboard/cards/LogsCard.svelte new file mode 100644 index 00000000..09beebb5 --- /dev/null +++ b/frontend/src/components/Dashboard/cards/LogsCard.svelte @@ -0,0 +1,39 @@ + + +
+ +
+ + \ No newline at end of file diff --git a/frontend/src/components/views/LogsView.svelte b/frontend/src/components/views/LogsView.svelte index 124d5bd6..cd6a4936 100644 --- a/frontend/src/components/views/LogsView.svelte +++ b/frontend/src/components/views/LogsView.svelte @@ -3,6 +3,7 @@ import { apiSSE } from '../../services/api'; // Main state + let { hideTimeRange = false } = $props(); let logs = $state([]); let filteredLogs = $state([]); let logSources = $state({ @@ -274,6 +275,7 @@ + {#if !hideTimeRange}
+ {/if}
From b5f65f4bb355f9443cd110309a570bceaa74143c Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 18 Oct 2025 03:03:58 +0200 Subject: [PATCH 56/93] add socket proxy handler and endpoit to register routes that are passed to the plugins API --- src/api/pluginproxy/socketproxy.go | 80 +++++++++++++++++++++++++++++ src/api/pluginsapi/registerroute.go | 50 ++++++++++++++++++ src/api/routes.go | 3 ++ 3 files changed, 133 insertions(+) create mode 100644 src/api/pluginproxy/socketproxy.go create mode 100644 src/api/pluginsapi/registerroute.go diff --git a/src/api/pluginproxy/socketproxy.go b/src/api/pluginproxy/socketproxy.go new file mode 100644 index 00000000..6bc3ca9a --- /dev/null +++ b/src/api/pluginproxy/socketproxy.go @@ -0,0 +1,80 @@ +package pluginproxy + +import ( + "bufio" + "io" + "net" + "net/http" + "strings" + + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" +) + +func UnixSocketProxyHandler(socketPath string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // Extract the subpath after /plugins/ExamplePlugin/ + subPath := strings.TrimPrefix(r.URL.Path, "/plugins/ExamplePlugin") + if subPath == "" { + subPath = "/" // Default to root if no subpath + } + + // Dial the Unix domain socket + conn, err := net.Dial("unix", socketPath) + if err != nil { + logger.Plugin.Debugf("Failed to connect to Unix socket: %v", err) + http.Error(w, "Failed to connect to Unix socket: "+err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + + // Construct a full HTTP request with the subpath + var requestBuilder strings.Builder + requestBuilder.WriteString(r.Method + " " + subPath + " HTTP/1.1\r\n") + requestBuilder.WriteString("Host: localhost\r\n") + requestBuilder.WriteString("\r\n") + + // Read the request body + body, err := io.ReadAll(r.Body) + if err != nil { + logger.Plugin.Debugf("Failed to read request body: %v", err) + http.Error(w, "Failed to read request body: "+err.Error(), http.StatusBadRequest) + return + } + requestBuilder.Write(body) + + // Send the HTTP request to the Unix socket + _, err = conn.Write([]byte(requestBuilder.String())) + if err != nil { + logger.Plugin.Debugf("Failed to write to Unix socket: %v", err) + http.Error(w, "Failed to write to Unix socket: "+err.Error(), http.StatusInternalServerError) + return + } + + // Read the response from the Unix socket + reader := bufio.NewReader(conn) + resp, err := http.ReadResponse(reader, r) + if err != nil { + logger.Plugin.Debugf("Failed to read response from Unix socket: %v", err) + http.Error(w, "Failed to read response from Unix socket: "+err.Error(), http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + // Copy headers from the socket response + for key, values := range resp.Header { + for _, value := range values { + w.Header().Add(key, value) + } + } + + // Set the status code + w.WriteHeader(resp.StatusCode) + + // Copy the response body + _, err = io.Copy(w, resp.Body) + if err != nil { + logger.Plugin.Debugf("Failed to write response to client: %v", err) + } + } +} diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go new file mode 100644 index 00000000..b6956416 --- /dev/null +++ b/src/api/pluginsapi/registerroute.go @@ -0,0 +1,50 @@ +package pluginsapi + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/SteamServerUI/SteamServerUI/v7/src/api/pluginproxy" + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" +) + +func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protectedMux *http.ServeMux) { + w.Header().Set("Content-Type", "application/json") + + // Only handle POST requests + if r.Method != http.MethodPost { + http.Error(w, `{"status":"error","message":"Method not allowed"}`, http.StatusMethodNotAllowed) + return + } + + // Define struct for incoming JSON + type registerRequest struct { + PluginName string `json:"pluginname"` + } + + // Decode JSON request body + var req registerRequest + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&req); err != nil { + http.Error(w, `{"status":"error","message":"Invalid JSON format"}`, http.StatusBadRequest) + return + } + + // Validate input + if req.PluginName == "" { + http.Error(w, `{"status":"error","message":"Missing required field pluginname"}`, http.StatusBadRequest) + return + } + + logger.API.Infof("Registering plugin in API: %s", req.PluginName) + + // Dynamically register the plugin route in protectedMux + route := fmt.Sprintf("/plugins/%s/", req.PluginName) + socketPath := fmt.Sprintf("/tmp/ssui/%s.sock", req.PluginName) + protectedMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) + + // Write success response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} diff --git a/src/api/routes.go b/src/api/routes.go index f0bf28f6..2f93d5ed 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -118,4 +118,7 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { // SetupSocketAPIRoutes adds routes that are E X C L U S I V E L Y available via sockets (if debug mode is enabled, these routes are added to the http api as well) func SetupSocketAPIRoutes(APIMux *http.ServeMux) { APIMux.HandleFunc("/api/v2/plugins/log", pluginsapi.PluginLogHandler) + APIMux.HandleFunc("/api/v2/plugins/register", func(w http.ResponseWriter, r *http.Request) { + pluginsapi.RegisterPluginRouteHandler(w, r, APIMux) + }) } From 8f4101bc7301aaed4e8026698cfa2eb3afd07925 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 18 Oct 2025 03:38:01 +0200 Subject: [PATCH 57/93] added route already registered check --- src/api/pluginsapi/registerroute.go | 24 ++++++++++++++++++++++++ src/api/socketapi/socket-lin.go | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go index b6956416..23bc4c8c 100644 --- a/src/api/pluginsapi/registerroute.go +++ b/src/api/pluginsapi/registerroute.go @@ -4,11 +4,15 @@ import ( "encoding/json" "fmt" "net/http" + "sync" "github.com/SteamServerUI/SteamServerUI/v7/src/api/pluginproxy" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) +var pluginRoutes = make(map[string]bool) +var pluginRoutesMu sync.Mutex + func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protectedMux *http.ServeMux) { w.Header().Set("Content-Type", "application/json") @@ -42,9 +46,29 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protecte // Dynamically register the plugin route in protectedMux route := fmt.Sprintf("/plugins/%s/", req.PluginName) socketPath := fmt.Sprintf("/tmp/ssui/%s.sock", req.PluginName) + + err := checkRoute(route) + if err { + http.Error(w, `{"status":"error","message":"Plugin route already registered"}`, http.StatusConflict) + return + } + protectedMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) // Write success response w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"status": "success"}) } + +func checkRoute(route string) (registered bool) { + // Check if the route is already registered + pluginRoutesMu.Lock() + defer pluginRoutesMu.Unlock() + + if pluginRoutes[route] { + return true + } + // save the route in the plugin routes map + pluginRoutes[route] = true + return false +} diff --git a/src/api/socketapi/socket-lin.go b/src/api/socketapi/socket-lin.go index 52a696cf..462f09bb 100644 --- a/src/api/socketapi/socket-lin.go +++ b/src/api/socketapi/socket-lin.go @@ -45,7 +45,7 @@ func StartSocketServer(wg *sync.WaitGroup) { } // Set socket permissions - if err := os.Chmod(socketPath, 0666); err != nil { + if err := os.Chmod(socketPath, 0600); err != nil { logger.Socket.Error("Error setting socket permissions: " + err.Error()) } From cf9a2805bcc73ad793b17cea5ceaf6496967b5d6 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 18 Oct 2025 03:43:40 +0200 Subject: [PATCH 58/93] change socket permission to 0600 --- src/api/pluginsapi/registerroute.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go index 23bc4c8c..6b7a577e 100644 --- a/src/api/pluginsapi/registerroute.go +++ b/src/api/pluginsapi/registerroute.go @@ -41,8 +41,6 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protecte return } - logger.API.Infof("Registering plugin in API: %s", req.PluginName) - // Dynamically register the plugin route in protectedMux route := fmt.Sprintf("/plugins/%s/", req.PluginName) socketPath := fmt.Sprintf("/tmp/ssui/%s.sock", req.PluginName) @@ -53,6 +51,8 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protecte return } + logger.Plugin.Infof("Registering plugin in API: %s", req.PluginName) + protectedMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) // Write success response From 2b816198ff8d09aa8195a15908ef9af73f3718fa Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 18 Oct 2025 04:10:01 +0200 Subject: [PATCH 59/93] improved responses for plugin proxy --- src/api/pluginsapi/registerroute.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go index 6b7a577e..6f0b8d8b 100644 --- a/src/api/pluginsapi/registerroute.go +++ b/src/api/pluginsapi/registerroute.go @@ -47,17 +47,17 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protecte err := checkRoute(route) if err { - http.Error(w, `{"status":"error","message":"Plugin route already registered"}`, http.StatusConflict) + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(map[string]string{"status": "failed", "message": "Plugin route already registered"}) return } - logger.Plugin.Infof("Registering plugin in API: %s", req.PluginName) - protectedMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) + logger.Plugin.Infof("Registered %s plugin route %s in API", req.PluginName, route) // Write success response w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "Plugin route registered successfully"}) } func checkRoute(route string) (registered bool) { From 7185932f580312abbe6f3bb6df77e0fc33f7f8cd Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 18 Oct 2025 04:16:34 +0200 Subject: [PATCH 60/93] added global reference to webserver mux to fix dynamic route registration --- src/api/pluginsapi/registerroute.go | 4 ++-- src/api/routes.go | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go index 6f0b8d8b..2506be54 100644 --- a/src/api/pluginsapi/registerroute.go +++ b/src/api/pluginsapi/registerroute.go @@ -13,7 +13,7 @@ import ( var pluginRoutes = make(map[string]bool) var pluginRoutesMu sync.Mutex -func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protectedMux *http.ServeMux) { +func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, apiMux *http.ServeMux, webserverMux *http.ServeMux) { w.Header().Set("Content-Type", "application/json") // Only handle POST requests @@ -52,7 +52,7 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, protecte return } - protectedMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) + webserverMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) logger.Plugin.Infof("Registered %s plugin route %s in API", req.PluginName, route) // Write success response diff --git a/src/api/routes.go b/src/api/routes.go index 2f93d5ed..06d442e7 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -19,6 +19,8 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/settings" ) +var GlobalWebProtectedMux *http.ServeMux + // SetupAPIRoutes sets up API routes used by B O T H the web and socket servers func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { @@ -34,6 +36,7 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { // Protected routes (wrapped with middleware) protectedMux := http.NewServeMux() + GlobalWebProtectedMux = protectedMux legacyAssetsFS, _ := fs.Sub(config.GetV1UIFS(), "SSUI/onboard_bundled/assets") protectedMux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(legacyAssetsFS)))) @@ -119,6 +122,6 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { func SetupSocketAPIRoutes(APIMux *http.ServeMux) { APIMux.HandleFunc("/api/v2/plugins/log", pluginsapi.PluginLogHandler) APIMux.HandleFunc("/api/v2/plugins/register", func(w http.ResponseWriter, r *http.Request) { - pluginsapi.RegisterPluginRouteHandler(w, r, APIMux) + pluginsapi.RegisterPluginRouteHandler(w, r, APIMux, GlobalWebProtectedMux) }) } From 7b4bbd844894dc421b202bb154a00673afd331dd Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 18 Oct 2025 04:55:32 +0200 Subject: [PATCH 61/93] added some sanity checking to plugin proxy --- src/api/pluginsapi/registerroute.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go index 2506be54..6648fc56 100644 --- a/src/api/pluginsapi/registerroute.go +++ b/src/api/pluginsapi/registerroute.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "net/http" + "os" + "regexp" "sync" "github.com/SteamServerUI/SteamServerUI/v7/src/api/pluginproxy" @@ -40,11 +42,22 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, apiMux * http.Error(w, `{"status":"error","message":"Missing required field pluginname"}`, http.StatusBadRequest) return } + // sanatize plugin name (allow alphanumeric, underscores, and hyphens only) + if !isValidPluginName(req.PluginName) { + http.Error(w, `{"status":"error","message":"Invalid plugin name. Use only alphanumeric characters, underscores, or hyphens"}`, http.StatusBadRequest) + return + } - // Dynamically register the plugin route in protectedMux route := fmt.Sprintf("/plugins/%s/", req.PluginName) socketPath := fmt.Sprintf("/tmp/ssui/%s.sock", req.PluginName) + // check if the plugin socket exists + if !pluginSocketExists(socketPath) { + w.WriteHeader(http.StatusNotImplemented) + json.NewEncoder(w).Encode(map[string]string{"status": "failed", "message": "Plugin socket does not exist. Make sure to call PluginLib.ExposeAPI before calling PluginLib.RegisterPluginAPI"}) + return + } + err := checkRoute(route) if err { w.WriteHeader(http.StatusConflict) @@ -72,3 +85,15 @@ func checkRoute(route string) (registered bool) { pluginRoutes[route] = true return false } + +func isValidPluginName(name string) bool { + // Allow alphanumeric, underscores, and hyphens (minimum 1 character, maximum 50 characters) + pattern := `^[a-zA-Z0-9_-]{1,50}$` + matched, err := regexp.MatchString(pattern, name) + return err == nil && matched +} + +func pluginSocketExists(socketPath string) bool { + _, err := os.Stat(socketPath) + return err == nil +} From f572095565cb1f9c0e5d98752ddd0721f4ddc137 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 19 Oct 2025 03:02:14 +0200 Subject: [PATCH 62/93] fix UnixSocketProxyHandler to support dynamic plugin names for route registration --- src/api/pluginproxy/socketproxy.go | 5 ++--- src/api/pluginsapi/registerroute.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/pluginproxy/socketproxy.go b/src/api/pluginproxy/socketproxy.go index 6bc3ca9a..7b007050 100644 --- a/src/api/pluginproxy/socketproxy.go +++ b/src/api/pluginproxy/socketproxy.go @@ -10,11 +10,10 @@ import ( "github.com/SteamServerUI/SteamServerUI/v7/src/logger" ) -func UnixSocketProxyHandler(socketPath string) http.HandlerFunc { +func UnixSocketProxyHandler(socketPath string, pluginName string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Extract the subpath after /plugins/ExamplePlugin/ - subPath := strings.TrimPrefix(r.URL.Path, "/plugins/ExamplePlugin") + subPath := strings.TrimPrefix(r.URL.Path, "/plugins/"+pluginName) if subPath == "" { subPath = "/" // Default to root if no subpath } diff --git a/src/api/pluginsapi/registerroute.go b/src/api/pluginsapi/registerroute.go index 6648fc56..52b96a69 100644 --- a/src/api/pluginsapi/registerroute.go +++ b/src/api/pluginsapi/registerroute.go @@ -65,7 +65,7 @@ func RegisterPluginRouteHandler(w http.ResponseWriter, r *http.Request, apiMux * return } - webserverMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath)) + webserverMux.HandleFunc(route, pluginproxy.UnixSocketProxyHandler(socketPath, req.PluginName)) logger.Plugin.Infof("Registered %s plugin route %s in API", req.PluginName, route) // Write success response From d1854b92904c943855c37a9de85ac525eac73d09 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 19 Oct 2025 03:08:07 +0200 Subject: [PATCH 63/93] enhance UnixSocketProxyHandler to include query params --- src/api/pluginproxy/socketproxy.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/api/pluginproxy/socketproxy.go b/src/api/pluginproxy/socketproxy.go index 7b007050..f394fdeb 100644 --- a/src/api/pluginproxy/socketproxy.go +++ b/src/api/pluginproxy/socketproxy.go @@ -12,11 +12,15 @@ import ( func UnixSocketProxyHandler(socketPath string, pluginName string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - + // Trim the plugin prefix from the URL path subPath := strings.TrimPrefix(r.URL.Path, "/plugins/"+pluginName) if subPath == "" { subPath = "/" // Default to root if no subpath } + requestPath := subPath + if r.URL.RawQuery != "" { + requestPath += "?" + r.URL.RawQuery + } // Dial the Unix domain socket conn, err := net.Dial("unix", socketPath) @@ -27,10 +31,16 @@ func UnixSocketProxyHandler(socketPath string, pluginName string) http.HandlerFu } defer conn.Close() - // Construct a full HTTP request with the subpath var requestBuilder strings.Builder - requestBuilder.WriteString(r.Method + " " + subPath + " HTTP/1.1\r\n") + requestBuilder.WriteString(r.Method + " " + requestPath + " HTTP/1.1\r\n") requestBuilder.WriteString("Host: localhost\r\n") + + // Copy headers from the original request + for key, values := range r.Header { + for _, value := range values { + requestBuilder.WriteString(key + ": " + value + "\r\n") + } + } requestBuilder.WriteString("\r\n") // Read the request body @@ -42,7 +52,6 @@ func UnixSocketProxyHandler(socketPath string, pluginName string) http.HandlerFu } requestBuilder.Write(body) - // Send the HTTP request to the Unix socket _, err = conn.Write([]byte(requestBuilder.String())) if err != nil { logger.Plugin.Debugf("Failed to write to Unix socket: %v", err) @@ -50,7 +59,6 @@ func UnixSocketProxyHandler(socketPath string, pluginName string) http.HandlerFu return } - // Read the response from the Unix socket reader := bufio.NewReader(conn) resp, err := http.ReadResponse(reader, r) if err != nil { From 80b9c9fca122acc94609e57bf98c24ebf932bb7a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 19 Oct 2025 04:29:47 +0200 Subject: [PATCH 64/93] added /api/v2/runfile/args/getarg to get a single arg from rf --- src/api/routes.go | 1 + src/api/runfileapi/runfile.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/api/routes.go b/src/api/routes.go index 06d442e7..db42ec1b 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -101,6 +101,7 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/runfile/groups", runfileapi.HandleRunfileGroups) protectedMux.HandleFunc("/api/v2/runfile/args", runfileapi.HandleRunfileArgs) protectedMux.HandleFunc("/api/v2/runfile/args/update", runfileapi.HandleRunfileArgUpdate) + protectedMux.HandleFunc("/api/v2/runfile/args/getarg", runfileapi.HandleRunfileGetArg) protectedMux.HandleFunc("/api/v2/runfile", runfileapi.HandleRunfile) protectedMux.HandleFunc("/api/v2/runfile/save", runfileapi.HandleRunfileSave) protectedMux.HandleFunc("/api/v2/runfile/hardreset", runfileapi.HandleSetRunfileGame) diff --git a/src/api/runfileapi/runfile.go b/src/api/runfileapi/runfile.go index 968b7d17..88efe413 100644 --- a/src/api/runfileapi/runfile.go +++ b/src/api/runfileapi/runfile.go @@ -155,6 +155,40 @@ func HandleRunfileArgs(w http.ResponseWriter, r *http.Request) { writeJSONResponse(w, http.StatusOK, apiArgs, "") } +func HandleRunfileGetArg(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + type Request struct { + Flag string `json:"flag"` + } + type Response struct { + Value string `json:"value,omitempty"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + } + + var req Request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest) + return + } + + if runfile.CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + json.NewEncoder(w).Encode(Response{Status: "failed", Error: "runfile not loaded"}) + w.WriteHeader(http.StatusInternalServerError) + return + } + + value := runfile.CurrentRunfile.GetArgValue(req.Flag) + if value == "" { + json.NewEncoder(w).Encode(Response{Status: "failed", Error: "arg not found"}) + w.WriteHeader(http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(Response{Value: value, Status: "success"}) +} + // HandleRunfileArgUpdate handles POST /api/v2/runfile/args/update func HandleRunfileArgUpdate(w http.ResponseWriter, r *http.Request) { logger.Runfile.Debug("POST /api/v2/runfile/args") From 8eddb7ae28f97c750c48c09f2719a8c5076f7df8 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 19 Oct 2025 05:40:09 +0200 Subject: [PATCH 65/93] added dynamic plugins views to frontend - plugins dynmically register and render in frontend, updated every 25 sec. If no plugins are registered, view is hidden. - added according backend api route /api/v2/plugins/list to get the currently registered plugins --- frontend/src/App.svelte | 35 ++++++- .../Dashboard/cards/LogsCard.svelte | 2 +- frontend/src/components/MainContent.svelte | 9 ++ frontend/src/components/nav/Sidebar.svelte | 2 + .../components/plugins/PluginContainer.svelte | 76 +++++++++++++++ .../src/components/plugins/PluginsView.svelte | 95 +++++++++++++++++++ frontend/src/services/plugins.js | 4 + src/api/pluginsapi/listplugins.go | 14 +++ src/api/routes.go | 3 + 9 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/plugins/PluginContainer.svelte create mode 100644 frontend/src/components/plugins/PluginsView.svelte create mode 100644 frontend/src/services/plugins.js create mode 100644 src/api/pluginsapi/listplugins.go diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index a9178b21..7ba1c004 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -4,20 +4,23 @@ import MainContent from './components/MainContent.svelte'; import BackendInitializer from './BackendInitializer.svelte'; import AuthGuard from './AuthGuard.svelte'; + import { apiFetch } from './services/api'; + import { pluginsList } from './services/plugins'; import './themes/theme.css'; import ScreenNotSupported from './components/resuables/ScreenNotSupported.svelte'; // Track active view let activeView = $state('dashboard'); + let hasPlugins = $state(false); // Views available in the app - const views = [ + let views = $state([ { id: 'dashboard', name: 'Dashboard', icon: 'grid' }, { id: 'settings', name: 'Settings', icon: 'settings' }, { id: 'console', name: 'Console', icon: 'terminal' }, { id: 'logs', name: 'Logs', icon: 'file-text' }, { id: 'gallery', name: 'Gallery', icon: 'globe' }, - ]; + ]); // Set active view function function setActiveView(viewId) { @@ -46,12 +49,40 @@ forceShowApp = true; } + // Check if plugins are available + async function checkPlugins() { + try { + const response = await apiFetch('/api/v2/plugins/list'); + const data = await response.json(); + hasPlugins = Array.isArray(data) && data.length > 0; + + // Store plugins data for use in PluginsView + pluginsList.set(data || []); + + // Add plugins view if plugins exist + if (hasPlugins) { + views = [ + ...views, + { id: 'plugins', name: 'Plugins', icon: 'plugin' }, + ]; + } + } catch (error) { + console.error('Error checking plugins:', error); + hasPlugins = false; + pluginsList.set([]); + } + } + // Run on mount and on window resize $effect(() => { checkScreenSize(); + checkPlugins(); window.addEventListener('resize', checkScreenSize); return () => window.removeEventListener('resize', checkScreenSize); }); + + // run checkPlugins every 25 seconds + setInterval(checkPlugins, 25000); {#if isScreenSupported || forceShowApp} diff --git a/frontend/src/components/Dashboard/cards/LogsCard.svelte b/frontend/src/components/Dashboard/cards/LogsCard.svelte index 09beebb5..65953cbd 100644 --- a/frontend/src/components/Dashboard/cards/LogsCard.svelte +++ b/frontend/src/components/Dashboard/cards/LogsCard.svelte @@ -1,5 +1,5 @@
diff --git a/frontend/src/components/MainContent.svelte b/frontend/src/components/MainContent.svelte index c1550d1d..13428153 100644 --- a/frontend/src/components/MainContent.svelte +++ b/frontend/src/components/MainContent.svelte @@ -5,6 +5,7 @@ import LogsView from './views/LogsView.svelte'; import ConsoleView from './views/ConsoleView.svelte'; import RunfileGalleryView from './views/RunfileGalleryView.svelte'; + import PluginsView from './plugins/PluginsView.svelte'; /** * @typedef {Object} Props @@ -40,6 +41,10 @@ title: 'Runfile Gallery', description: 'Browse runfiles' }, + plugins: { + title: 'Plugins', + description: 'Manage plugins' + } }; @@ -70,6 +75,10 @@
+ {:else if activeView === 'plugins'} +
+ +
{/if}
diff --git a/frontend/src/components/nav/Sidebar.svelte b/frontend/src/components/nav/Sidebar.svelte index 566fe372..4d41cf83 100644 --- a/frontend/src/components/nav/Sidebar.svelte +++ b/frontend/src/components/nav/Sidebar.svelte @@ -63,6 +63,8 @@ 🌐 {:else if view.icon === 'archive'} 📦 + {:else if view.icon === 'plugin'} + 🔌 {/if} {view.name} diff --git a/frontend/src/components/plugins/PluginContainer.svelte b/frontend/src/components/plugins/PluginContainer.svelte new file mode 100644 index 00000000..0361d785 --- /dev/null +++ b/frontend/src/components/plugins/PluginContainer.svelte @@ -0,0 +1,76 @@ + + +
+ +
+ +{#if loading} +
+

Loading plugin...

+
+{/if} + + \ No newline at end of file diff --git a/frontend/src/components/plugins/PluginsView.svelte b/frontend/src/components/plugins/PluginsView.svelte new file mode 100644 index 00000000..3667d07f --- /dev/null +++ b/frontend/src/components/plugins/PluginsView.svelte @@ -0,0 +1,95 @@ + + +
+
+ {#each plugins as pluginPath} + + {/each} +
+ +
+ {#if activeSidebarTab} + + {/if} +
+
+ + \ No newline at end of file diff --git a/frontend/src/services/plugins.js b/frontend/src/services/plugins.js new file mode 100644 index 00000000..dcb760d5 --- /dev/null +++ b/frontend/src/services/plugins.js @@ -0,0 +1,4 @@ + +import { writable } from 'svelte/store'; + +export const pluginsList = writable([]); \ No newline at end of file diff --git a/src/api/pluginsapi/listplugins.go b/src/api/pluginsapi/listplugins.go new file mode 100644 index 00000000..46ad0deb --- /dev/null +++ b/src/api/pluginsapi/listplugins.go @@ -0,0 +1,14 @@ +package pluginsapi + +import ( + "encoding/json" + "net/http" +) + +func HandleListPlugins(w http.ResponseWriter, r *http.Request) { + plugins := make([]string, 0) + for plugin := range pluginRoutes { + plugins = append(plugins, plugin) + } + json.NewEncoder(w).Encode(plugins) +} diff --git a/src/api/routes.go b/src/api/routes.go index db42ec1b..48c90f73 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -116,6 +116,9 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/gallery", runfileapi.GalleryHandler) protectedMux.HandleFunc("/api/v2/gallery/select", runfileapi.GallerySelectHandler) + // --- PLUGINS --- + protectedMux.HandleFunc("/api/v2/plugins/list", pluginsapi.HandleListPlugins) + return mux, protectedMux } From 3b2e72ad35f1c44f4e8da243de0a76f23daa8e3a Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 19 Oct 2025 06:26:26 +0200 Subject: [PATCH 66/93] fix plugins view would show multiple times in navbars --- frontend/src/App.svelte | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 7ba1c004..998d7c31 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -61,10 +61,14 @@ // Add plugins view if plugins exist if (hasPlugins) { - views = [ - ...views, - { id: 'plugins', name: 'Plugins', icon: 'plugin' }, - ]; + // check if plugins view already exists + const existingPluginsView = views.find(view => view.id === 'plugins'); + if (!existingPluginsView) { + views = [ + ...views, + { id: 'plugins', name: 'Plugins', icon: 'plugin' }, + ]; + } } } catch (error) { console.error('Error checking plugins:', error); From 00ff4af8125939aa9a1b1eb9bd04236ec2e3a819 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sun, 19 Oct 2025 19:24:15 +0200 Subject: [PATCH 67/93] re-added the Desktop App & Backend CORS handling for it. --- .devcontainer/Dockerfile | 14 + .gitignore | 4 +- .vscode/tasks.json | 47 +- build/.version | 1 + build/build.go | 67 +- frontend/main.cjs | 258 + frontend/package-lock.json | 7369 ++++++++++++++++++++++---- frontend/package.json | 12 +- media/logo.png | Bin 0 -> 219074 bytes src/api/httpauth/activate.go | 2 + src/api/httpauth/login.go | 2 + src/api/httpauth/logout.go | 3 + src/api/httpauth/registerapikey.go | 2 + src/api/httpauth/registeruser.go | 2 + src/api/middleware/authmiddleware.go | 23 + 15 files changed, 6691 insertions(+), 1115 deletions(-) create mode 100644 build/.version create mode 100644 frontend/main.cjs create mode 100644 media/logo.png diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c87c9209..b76e55a2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -27,6 +27,20 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ apt-get install -y nodejs && \ npm install -g npm@latest +# Install Wine for Windows .exe builds +RUN dpkg --add-architecture i386 && \ + apt-get update && \ + apt-get install -y wine wine32 wine64 && \ + rm -rf /var/lib/apt/lists/* + +# Install NSIS for Windows NSIS installer +RUN wget -q https://downloads.sourceforge.net/project/nsis/NSIS%203/3.09/nsis-3.09-setup.exe && \ + wine nsis-3.09-setup.exe /S && \ + rm nsis-3.09-setup.exe + +# Start D-Bus to avoid Electron errors +RUN service dbus start + # Set up non-root user and workspace RUN mkdir -p /tmp/ssui && chmod 777 /tmp/ssui diff --git a/.gitignore b/.gitignore index cb39947c..515ec4e9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,8 @@ rocketstation_DedicatedServer* .github/workflows/nightly-sync.yml repos.md SSUI/*.pem -build/StationeersServerControl* +build/SteamServerUI* +build/release Blacklist.txt SSUI/detectionmanager/customdetections.json SSUI/tls/cert.pem @@ -41,6 +42,7 @@ winhttp.dll autostart* __debug_bin* frontend/node_modules +frontend/dist_electron frontend/dist frontend/build SSUI/onboard_bundled/v2 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 377aed6d..1f0b6364 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,8 +13,7 @@ }, "problemMatcher": [], "group": "build", - "detail": "Install Svelte UI dependencies", - "hide": true + "detail": "Install Svelte UI dependencies" }, { "type": "shell", @@ -52,7 +51,32 @@ }, { "type": "shell", - "label": "Build: Go Server with fresh assets", + "label": "npm: build electron & svelte", + "command": "npm", + "args": [ + "run", + "electron" + ], + "options": { + "cwd": "${workspaceFolder}/frontend" + }, + "problemMatcher": [], + "group": "test", + "isBackground": true, + "presentation": { + "reveal": "always", + "panel": "dedicated" + }, + "dependsOn": [ + "go: sync backend version with frontend version", + "npm: build" + ], + "dependsOrder": "sequence", + "detail": "Build Electron App for Linux and Windows" + }, + { + "type": "shell", + "label": "go: run build.go", "command": "go", "args": [ "run", @@ -67,7 +91,22 @@ "npm: build" ], "dependsOrder": "sequence", - "hide": false + "hide": true + }, + { + "label": "Build: Full Project (Prep a release)", + "dependsOn": [ + "npm: install", + "npm: build electron & svelte", + "go: run build.go" + ], + "dependsOrder": "sequence", + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "Complete build pipeline to prepare all files needed to ship release", + "problemMatcher": [] }, { "label": "Run: Go Server with fresh assets", diff --git a/build/.version b/build/.version new file mode 100644 index 00000000..41225218 --- /dev/null +++ b/build/.version @@ -0,0 +1 @@ +7.0.0 \ No newline at end of file diff --git a/build/build.go b/build/build.go index 6848b80c..4d178df1 100644 --- a/build/build.go +++ b/build/build.go @@ -34,10 +34,18 @@ func main() { // Load the config config.LoadConfig() + + version := config.GetVersion() fmt.Printf("%s✓ Configuration loaded%s\n", colorGreen, colorReset) + err := os.WriteFile("build/.version", []byte(version), 0644) + if err != nil { + fmt.Printf("%s✗ Failed to write version to file: %s%s\n", colorRed, err, colorReset) + } + // Increment the version - newVersion := incrementVersion("./src/config/config.go") + //newVersion := incrementVersion("./src/config/config.go") + newVersion := version // Platforms to build for platforms := []struct { @@ -76,7 +84,7 @@ func main() { } // Output to /build - outputPath := filepath.Join("build", outputName) + outputPath := filepath.Join("build/release", outputName) // Run the go build command targeting server.go at root cmd := exec.Command("go", "build", "-ldflags=-s -w", "-gcflags=-l=4", "-o", outputPath, "server.go") @@ -92,6 +100,11 @@ func main() { fmt.Printf("%s✓ Build successful!%s Created: %s%s%s\n", colorGreen, colorReset, colorYellow, outputPath, colorReset) } + + err = copyElectronFiles() + if err != nil { + log.Fatalf("Failed to copy Electron files: %v", err) + } fmt.Printf("%s\n=== Build Pipeline Completed ===%s\n", colorCyan, colorReset) } @@ -186,3 +199,53 @@ func cleanupOldExecutables(buildVersion string) { fmt.Printf("%s✓ Cleaned up %d old executable(s)%s\n", colorGreen, deletedCount, colorReset) } } + +func copyElectronFiles() error { + fmt.Printf("%s\nCopying Electron files...%s\n", colorBlue, colorReset) + + // Source and destination directories + srcDir := "./frontend/dist_electron" + destDir := "./build/release" + + // Ensure destination directory exists + if err := os.MkdirAll(destDir, 0755); err != nil { + fmt.Printf("Error creating build directory: %v\n", err) + return err + } + + // Patterns for files to copy + patterns := []string{"SSUI-Desktop*.deb", "SSUI-Desktop*.exe", "latest-linux.yml", "latest.yml"} + + for _, pattern := range patterns { + // Find files matching the pattern + files, err := filepath.Glob(filepath.Join(srcDir, pattern)) + if err != nil { + fmt.Printf("Error finding files for pattern %s: %v\n", pattern, err) + continue + } + + for _, srcFile := range files { + // Get the base filename + fileName := filepath.Base(srcFile) + destFile := filepath.Join(destDir, fileName) + + // Open source file + srcData, err := os.ReadFile(srcFile) + if err != nil { + fmt.Printf("Error reading file %s: %v\n", srcFile, err) + continue + } + + // Write to destination + err = os.WriteFile(destFile, srcData, 0644) + if err != nil { + fmt.Printf("Error copying file %s to %s: %v\n", srcFile, destFile, err) + continue + } + + fmt.Printf("Copied %s to %s\n", srcFile, destFile) + } + } + + return nil +} diff --git a/frontend/main.cjs b/frontend/main.cjs new file mode 100644 index 00000000..202d9d9e --- /dev/null +++ b/frontend/main.cjs @@ -0,0 +1,258 @@ +const { app, BrowserWindow, Menu, dialog } = require('electron'); +const { autoUpdater } = require('electron-updater'); +const path = require('path'); +const fs = require('fs'); +const express = require('express'); +const http = require('http'); + +// Configure auto-updater +// Only check for updates in production builds +if (process.env.NODE_ENV !== 'development') { + autoUpdater.setFeedURL({ + provider: 'github', + owner: 'SteamServerUI', + repo: 'SteamServerUI' + }); + + // Disable automatic installation on Linux for security/stability + if (process.platform === 'linux') { + autoUpdater.autoInstallOnAppQuit = false; + autoUpdater.autoDownload = true; // Still auto-dwnload, just don't auto-install + } +} + +// Auto-updater event handlers +autoUpdater.on('checking-for-update', () => { + console.log('Checking for update...'); +}); + +autoUpdater.on('update-available', (info) => { + console.log('Update available:', info.version); + // Optional: Show notification to user + dialog.showMessageBox({ + type: 'info', + title: 'Update Available', + message: `A new version (${info.version}) is available. It will be downloaded in the background.`, + buttons: ['OK'] + }); +}); + +autoUpdater.on('update-not-available', (info) => { + console.log('Update not available.'); +}); + +autoUpdater.on('error', (err) => { + console.error('Auto-updater error:', err); +}); + +autoUpdater.on('download-progress', (progressObj) => { + let log_message = "Download speed: " + progressObj.bytesPerSecond; + log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'; + log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')'; + console.log(log_message); +}); + +autoUpdater.on('update-downloaded', (info) => { + console.log('Update downloaded'); + + if (process.platform === 'linux') { + // On Linux, show manual installation instructions + const updatePath = path.join(require('os').homedir(), '.cache', 'steamserverui-updater', 'pending'); + dialog.showMessageBox({ + type: 'info', + title: 'Update Downloaded', + message: `Update v${info.version} has been downloaded!\n\nFor security reasons, please install manually:\n\n1. Close this application\n2. Open terminal and run:\n sudo dpkg -i "${updatePath}/SSUI-Desktop-v${info.version}-linux.deb"\n\nOr double-click the downloaded .deb file in your file manager.`, + buttons: ['Open Download Folder', 'Later', 'Quit App'] + }).then((result) => { + if (result.response === 0) { + // Open the download folder + require('electron').shell.openPath(updatePath); + } else if (result.response === 2) { + // Quit the app so user can install manually + app.quit(); + } + }); + } else { + // Windows + dialog.showMessageBox({ + type: 'info', + title: 'Update Ready', + message: 'Update has been downloaded. The application will restart to apply the update.', + buttons: ['Restart Now', 'Later'] + }).then((result) => { + if (result.response === 0) { + autoUpdater.quitAndInstall(); + } + }); + } +}); + +// Static file server +let server; +let currentPortIndex = 0; +const DEFAULT_PORTS = [28080, 28888, 29090, 27070, 26060, 35050, 34040, 30303, 34320, 34899]; + +function startServer() { + const expressApp = express(); + + // Determine the assets directory path + let assetsPath; + + // In production + const prodPath = path.join(process.resourcesPath, 'SSUI/onboard_bundled/v2'); + + // Check if path exists + if (fs.existsSync(prodPath)) { + assetsPath = prodPath; + } else { + dialog.showErrorBox('Error', 'Could not find assets directory: ' + prodPath); + app.exit(1); + return false; + } + + console.log('Serving assets from:', assetsPath); + + // Serve static files from the assets directory + expressApp.use(express.static(assetsPath)); + + // Start the server + server = http.createServer(expressApp); + + return new Promise((resolve, reject) => { + server.listen(DEFAULT_PORTS[currentPortIndex], () => { + console.log(`Server running at http://localhost:${DEFAULT_PORTS[currentPortIndex]}`); + resolve(true); + }); + + server.on('error', (err) => { + console.error('Server error:', err); + if (err.code === 'EADDRINUSE' && currentPortIndex < DEFAULT_PORTS.length - 1) { + currentPortIndex++; + console.log(`Port ${DEFAULT_PORTS[currentPortIndex - 1]} in use, trying ${DEFAULT_PORTS[currentPortIndex]}`); + server.close(); + startServer().then(resolve).catch(reject); + } else { + let errorMsg = err.code === 'EADDRINUSE' ? 'All default ports are in use. Please free up a port and try again.' : err.message; + dialog.showErrorBox('Server Error', errorMsg); + reject(err); + } + }); + }); +} + +async function createWindow() { + // Start the local server first + const serverStarted = await startServer(); + if (!serverStarted) return; + + const win = new BrowserWindow({ + width: 1920, + height: 1080, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + } + }); + + // Load from local server instead of file + console.log(`Loading UI from: http://localhost:${DEFAULT_PORTS[currentPortIndex]}/index.html`); + win.loadURL(`http://localhost:${DEFAULT_PORTS[currentPortIndex]}/index.html`); + + // For debugging + //win.webContents.openDevTools(); +} + +// Create application menu with update check option +function createMenu() { + const template = [ + { + label: 'Update', + submenu: [ + { + label: 'Check for Updates', + click: () => { + autoUpdater.checkForUpdatesAndNotify(); + } + }, + { + label: 'About', + click: () => { + dialog.showMessageBox({ + type: 'info', + title: 'About Steam Server UI', + message: `SSUI Desktop ${app.getVersion()}\nCopyright © 2025 JacksonTheMaster`, + buttons: ['OK'] + }); + } + }, + { + label: 'Toggle DevTools', + accelerator: process.platform === 'darwin' ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', + click: (item, focusedWindow) => { + if (focusedWindow) { + focusedWindow.webContents.toggleDevTools(); + } + } + } + ] + } + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +app.commandLine.appendSwitch('ignore-certificate-errors'); + +app.whenReady().then(() => { + createWindow(); + createMenu(); + + // Check for updates after app is ready (but not in development) + if (!process.env.NODE_ENV || process.env.NODE_ENV === 'production') { + // Check for updates immediately + autoUpdater.checkForUpdatesAndNotify(); + + // Check for updates every 30 minutes + setInterval(() => { + autoUpdater.checkForUpdatesAndNotify(); + }, 30 * 60 * 1000); + } +}); + +app.on('window-all-closed', () => { + app.quit(); +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +app.on('quit', () => { + if (server) { + server.closeAllConnections(); + server.close(() => { + console.log('Server closed'); + }); + // Force quit after 5 seconds if server doesn't close + setTimeout(() => { + console.log('Forcing app exit after timeout'); + app.exit(0); + }, 5000); + } +}); + +// Minimal error handling +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); + dialog.showErrorBox('Unexpected Error', 'An unexpected error occurred: ' + err.message); + app.exit(1); +}); + +process.on('unhandledRejection', (err) => { + console.error('Unhandled Rejection:', err); + dialog.showErrorBox('Unexpected Error', 'An unexpected error occurred: ' + (err.message || err)); + app.exit(1); +}); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6d326d98..4ed6d252 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,343 +9,617 @@ "version": "v7.0.0", "license": "proprietary", "dependencies": { + "electron-updater": "^6.6.2", + "express": "^5.1.0", "https": "^1.0.0" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.1.1", "concurrently": "^9.1.2", + "electron": "^36.1.0", + "electron-builder": "^26.0.12", "svelte": "^5.23.1", "vite": "^6.3.5", "wait-on": "^8.0.3" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, "engines": { - "node": ">=18" + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], + "node_modules/@electron/asar": { + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", + "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, "engines": { - "node": ">=18" + "node": ">=10.12.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/asar/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, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, "engines": { - "node": ">=18" + "node": ">=12.13.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], + "node_modules/@electron/node-gyp/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, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], + "node_modules/@electron/node-gyp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">= 10.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", + "integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=14.14" } }, - "node_modules/@esbuild/netbsd-arm64": { + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { + "node_modules/@esbuild/android-arm": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { + "node_modules/@esbuild/android-arm64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -353,16 +627,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -370,16 +644,16 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -387,16 +661,16 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/darwin-x64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -404,16 +678,16 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -421,182 +695,186 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/freebsd-x64": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "freebsd" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/linux-arm": { "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@hapi/address": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", - "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/@hapi/formula": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", - "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/hoek": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/pinpoint": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/tlds": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz", - "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@hapi/topo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0.0" + "node": ">=18" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ - "arm" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" - ] + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" - ] + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -604,13 +882,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -618,13 +899,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "netbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -632,13 +916,16 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -646,55 +933,50 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", - "cpu": [ - "arm" + "openharmony" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -702,1084 +984,5416 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ - "loong64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", - "cpu": [ - "riscv64" + "win32" ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", - "cpu": [ - "riscv64" - ], + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", - "cpu": [ - "s390x" - ], + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", - "cpu": [ - "x64" - ], + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", - "cpu": [ - "x64" - ], + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", - "cpu": [ - "arm64" - ], + "node_modules/@hapi/tlds": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz", + "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", - "cpu": [ - "arm64" - ], + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", - "cpu": [ - "ia32" - ], + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "20 || >=22" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "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, - "license": "MIT" + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", + "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", + "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", + "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", + "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", + "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", + "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", + "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", + "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", + "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", + "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", + "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", + "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", + "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", + "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", + "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", + "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", + "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", + "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", + "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", + "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", + "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, + "peer": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz", + "integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.0.12.tgz", + "integrity": "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.2.18", + "@electron/fuses": "^1.8.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.7.0", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.0.11", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "plist": "3.1.0", + "resedit": "^1.7.0", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.0.12", + "electron-builder-squirrel-windows": "26.0.12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.0.11", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.0.11.tgz", + "integrity": "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", + "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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/chalk/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "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, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/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, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": 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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "optional": 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, + "license": "MIT", + "optional": 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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.0.12.tgz", + "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "36.9.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-36.9.5.tgz", + "integrity": "sha512-1UCss2IqxqujSzg/2jkRjuiT3G+EEXgd6UKB5kUekwQW1LJ6d4QCr8YItfC3Rr9VIGRDJ29eOERmnRNO1Eh+NA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.0.12.tgz", + "integrity": "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "dmg-builder": "26.0.12", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", + "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.0.11", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz", + "integrity": "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-updater": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz", + "integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.6.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "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, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-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, + "license": "ISC", + "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==", + "license": "MIT", + "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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "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, + "license": "ISC", + "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/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "MIT", + "optional": 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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", + "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/joi": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", + "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": 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, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "license": "MIT" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/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, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/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, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/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, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" + "minipass": "^3.0.0", + "yallist": "^4.0.0" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" + "node": ">= 8" } }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.3.7" + "bin": { + "mkdirp": "bin/cmd.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" + "node": ">=10" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": 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==", "license": "MIT" }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "peer": true, "bin": { - "acorn": "bin/acorn" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=0.4.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "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, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "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==", + "node_modules/node-abi": { + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "semver": "^7.3.5" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=10" } }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "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/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "semver": "^7.3.5" } }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "license": "Apache-2.0", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "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==", + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">= 0.4" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chalk/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==", + "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==", + "license": "MIT", + "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, "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "ee-first": "1.1.1" }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "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, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "wrappy": "1" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "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, "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "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==", + "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, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" + "aggregate-error": "^3.0.0" }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" + "engines": { + "node": ">=10" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 0.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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" }, "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" + "node": ">=12", + "npm": ">=6" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" } }, - "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==", + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, "engines": { - "node": ">=0.4.0" + "node": ">=10.4.0" } }, - "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==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "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" + } + ], "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 0.4" + "node": "^10 || ^12 || >=14" } }, - "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, - "license": "MIT" - }, - "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==", + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", "dev": true, "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, "engines": { - "node": ">= 0.4" + "node": ">=14.0.0" } }, - "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==", + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || >=14" } }, - "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==", + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, + "license": "ISC", "engines": { - "node": ">= 0.4" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "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==", + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", - "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": ">=0.4.0" } }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "node": ">=10" } }, - "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, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, "engines": { - "node": ">=6" + "node": ">= 0.10" } }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "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==", "dev": true, "license": "MIT" }, - "node_modules/esrap": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", - "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "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, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "engines": { + "node": ">=0.6" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" }, "engines": { - "node": ">= 6" + "node": ">= 0.10" } }, - "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, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" } }, - "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==", + "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, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "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==", + "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, - "license": "ISC", + "license": "MIT", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=0.10.0" } }, - "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==", + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", "dev": true, "license": "MIT", "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" + "pe-library": "^0.4.1" }, "engines": { - "node": ">= 0.4" + "node": ">=12", + "npm": ">=6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/jet2jet" } }, - "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==", + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "lowercase-keys": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, "engines": { "node": ">=8" } }, - "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==", + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 4" + } + }, + "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, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", + "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.2", + "@rollup/rollup-android-arm64": "4.50.2", + "@rollup/rollup-darwin-arm64": "4.50.2", + "@rollup/rollup-darwin-x64": "4.50.2", + "@rollup/rollup-freebsd-arm64": "4.50.2", + "@rollup/rollup-freebsd-x64": "4.50.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", + "@rollup/rollup-linux-arm-musleabihf": "4.50.2", + "@rollup/rollup-linux-arm64-gnu": "4.50.2", + "@rollup/rollup-linux-arm64-musl": "4.50.2", + "@rollup/rollup-linux-loong64-gnu": "4.50.2", + "@rollup/rollup-linux-ppc64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-musl": "4.50.2", + "@rollup/rollup-linux-s390x-gnu": "4.50.2", + "@rollup/rollup-linux-x64-gnu": "4.50.2", + "@rollup/rollup-linux-x64-musl": "4.50.2", + "@rollup/rollup-openharmony-arm64": "4.50.2", + "@rollup/rollup-win32-arm64-msvc": "4.50.2", + "@rollup/rollup-win32-ia32-msvc": "4.50.2", + "@rollup/rollup-win32-x64-msvc": "4.50.2", + "fsevents": "~2.3.2" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 18" } }, - "node_modules/https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", - "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "license": "ISC" }, - "node_modules/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==", + "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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 18" } }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.6" + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/joi": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", - "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, "dependencies": { - "@hapi/address": "^5.1.1", - "@hapi/formula": "^3.0.2", - "@hapi/hoek": "^11.0.7", - "@hapi/pinpoint": "^2.0.1", - "@hapi/tlds": "^1.1.1", - "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.0.0" + "type-fest": "^0.13.1" }, "engines": { - "node": ">= 20" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, "engines": { - "node": ">=6" + "node": ">= 18" } }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "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, - "license": "MIT" + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "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, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "dev": true, + "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==", "license": "MIT", + "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.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "dev": true, + "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==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "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==", "license": "MIT", + "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/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, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "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==", "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "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": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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, "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, "engines": { - "node": ">=12" + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=10" } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "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" - } - ], "license": "MIT", + "optional": true, "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=8" } }, - "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/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } }, - "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==", + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/rollup": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.2", - "@rollup/rollup-android-arm64": "4.50.2", - "@rollup/rollup-darwin-arm64": "4.50.2", - "@rollup/rollup-darwin-x64": "4.50.2", - "@rollup/rollup-freebsd-arm64": "4.50.2", - "@rollup/rollup-freebsd-x64": "4.50.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", - "@rollup/rollup-linux-arm-musleabihf": "4.50.2", - "@rollup/rollup-linux-arm64-gnu": "4.50.2", - "@rollup/rollup-linux-arm64-musl": "4.50.2", - "@rollup/rollup-linux-loong64-gnu": "4.50.2", - "@rollup/rollup-linux-ppc64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-musl": "4.50.2", - "@rollup/rollup-linux-s390x-gnu": "4.50.2", - "@rollup/rollup-linux-x64-gnu": "4.50.2", - "@rollup/rollup-linux-x64-musl": "4.50.2", - "@rollup/rollup-openharmony-arm64": "4.50.2", - "@rollup/rollup-win32-arm64-msvc": "4.50.2", - "@rollup/rollup-win32-ia32-msvc": "4.50.2", - "@rollup/rollup-win32-x64-msvc": "4.50.2", - "fsevents": "~2.3.2" + "node": ">= 10" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "node_modules/socks-proxy-agent/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, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "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, - "license": "MIT", + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/source-map-js": { @@ -1792,6 +6406,67 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1807,7 +6482,37 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "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, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "MIT", + "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==", @@ -1820,6 +6525,19 @@ "node": ">=8" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -1863,6 +6581,150 @@ "node": ">=18" } }, + "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, + "license": "ISC", + "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, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1880,6 +6742,35 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -1890,6 +6781,16 @@ "tree-kill": "cli.js" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1897,6 +6798,170 @@ "dev": true, "license": "0BSD" }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "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, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vite": { "version": "6.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", @@ -2013,6 +7078,32 @@ "node": ">=12.0.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -2031,6 +7122,41 @@ "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -2041,6 +7167,13 @@ "node": ">=10" } }, + "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, + "license": "ISC" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -2070,6 +7203,30 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index e0e63784..34a3dba8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,11 +2,11 @@ "name": "steamserverui", "main": "main.cjs", "private": true, - "version": "v7.0.1", + "version": "v7.0.0", "description": "Svelte UI Interface for Steam Server UI (SSUI) Backend", "author": { "name": "JacksonTheMaster", - "email": "ssui@jmg-it.de" + "email": "info@ssui.dev" }, "license": "proprietary", "repository": { @@ -17,15 +17,23 @@ "scripts": { "dev": "vite", "vite": "vite", + "electron": "vite build && electron-builder --linux --win", "build": "vite build", + "build:electron:dir": "vite build && electron-builder --linux --win --dir", + "build:electron": "vite build && electron-builder --linux --win", + "build:electron:win": "vite build && electron-builder --win", "preview": "vite preview" }, "dependencies": { + "electron-updater": "^6.6.2", + "express": "^5.1.0", "https": "^1.0.0" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.1.1", "concurrently": "^9.1.2", + "electron": "^36.1.0", + "electron-builder": "^26.0.12", "svelte": "^5.23.1", "vite": "^6.3.5", "wait-on": "^8.0.3" diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..12e9ca9723c051022e9dd29ee64325c6a91a4134 GIT binary patch literal 219074 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�^9*0UUzQ1zaVA@$E7SwsW{0^9* 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

|?un$di^8OM(u!~XpTvH##f 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

)UyxB{QG(sh9LM1Bv{aAf0+5b#FFVv~{0jm6# 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%0AT=uAt?`+M6n{T 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

!<3Yd;Okhe?vB`Oi?`Q_JkzPukz-RldCm<}LHM z^3Rsw6_y+ZH17M#I{mAK&6sN&MebWxKs^`9U>+fs-Zt%Hz&9j;JI_bFue>nk%`}!> zcz|Axe|7Ih8E}BabNcB8v!2|h&Z1}FYl8f%Z`|pSm`bC95L9piLZ9 zEyj=6n5OBlAO2U0_KPYJeMbhj(pI10(eR3n31d-8_t7(#3})Kg5l#z~@gAl<0Xx@` zIc2XRgppJK8i6edUrUn^6YZ>KWWCxvZJXlT-kEB$;ib5H_fXx#zILWKmlelXz89G< z`$b!Jy`m$7SUYj=+5-yn7(ORtD&QxmdU?tcT!gjin?9uWkzP=Cv1dXUj<@S+>8`G| zzLBPUE5E`m|ER>a)c!7;944k<9i9HBAXk~46%WW;&J*>F{_;^TjLa8rtbIqAh4!+` zm&bKaH0)f1tl?X^HwSvU zV#3QEj?tyu4&^vIQ$kh^n?4~L&9Xt%Osg+T{`@(R^x0-^Zwb=2yt5Fpw_CUoM6vW3 zeI`KKeb8ojq!J~EjttS|F@@u{xi1ogewpST8O85cx;(Bx~H)RS)Sx{4!$ z{$^egy@go6$&ANp+hPS&nM1Bui&{%|53M-W4q3;8QTXnXSba^D_qhN57y>yx9GIX}{{Is`4!ACB&5Jwsw-$Ze@P(+TH`+X*oOBqVwRRYW1wrp*> zG`m)atcsV_k&HA5w|}QklQx|F>>^cyHAZlc97VRvuSIe(k6zXb&P84fkyF$Ln9 z`DScZ9le6XHzDO$a~u7Gq6EuUrO&DH23MAlf@SXx`x>B%o-1Po)Dumc#d!|ZA}^Lf zPx>}f;WnRRtEHpu6I{1qG@@TS+_Z;zI+7p$`PKLB@VW<~hoslwavjEv)3(GE+;>T5 zWeNfx0ZfD7_ny#4c8b*Rrk{CEytjhmyEcZN5f*36wN-sHjZY+L*NrMY?obCk$(#4H2U>@xW0nIjNQHL{eO%U@%)^?~tghQXD;rK+9l1EJDQdH|@ z$1Q{FW29lzbUD`R0ls7vqD=%|LRt-A=Mrg0S3k9wn<>oF4kqTh?imTIez>0sBBH>@ zuNOW24t=+{qj)N#`+Y4)FES?~Il!1EF@;aKWdjJjG9l$X%dUp;Z`MU_D}3|{&9W~U zA??RSZ)K-?M;z@fJF><+qr^{TcCCYVX(^DF*_c)Ibg~-+;#=Fqk64#P6U2mk>o>x}-tuWc#qdUn1RQ8y`irzxlCxi~OW_ZQY{`xKELhy2fLDSCBD?>b090h) zBlqP7%?$m_bYB&fE*R{G&yubiyS_MtJaWVWA)wcyf_>8OJ2XigG@caP30e}iu#VI6 zbVZdKPlgQp$5_^pfXtwB7 zCi-Amw;FgD>v~iT>Vn+U!M4<`mk9Of)M%5{eAcm-hZJx2uj(vD_nJlc{m@?uHeS`8 zjBkM4Ze3I#xwaBCx8dKVTrdE`Bl14^?dNW>=B-Majb5~*Ft6Ero&1-S@(?M%Tw1hK z2D!Zvb=@^c+QGmfS-e98tM+}86@r4!3`EULd@{L_^exsL&B;FIF_%GH1XGnXJ`k79 z&y57mUEi59R}w!90LGXr@tan#+XH~+XVrxr1@A zL53Ln)U+{}-|jiEW}5(&+YvS>&n%eKY n?C`?BgZvl%znTAkUH1FD5(EU{MAROcgZ)`q*qK+r(XsynE7dGH literal 0 HcmV?d00001 diff --git a/src/api/httpauth/activate.go b/src/api/httpauth/activate.go index 1fbd0647..4fd4d604 100644 --- a/src/api/httpauth/activate.go +++ b/src/api/httpauth/activate.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/middleware" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/core/loader" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" @@ -11,6 +12,7 @@ import ( // ActivateAuthHandler (ex. SetupFinalizeHandler) marks setup as complete func ActivateAuthHandler(w http.ResponseWriter, r *http.Request) { + middleware.HandleCORS(w, r) //check if users map is nil or empty if len(config.GetUsers()) == 0 { diff --git a/src/api/httpauth/login.go b/src/api/httpauth/login.go index db69c2c8..4b3112fd 100644 --- a/src/api/httpauth/login.go +++ b/src/api/httpauth/login.go @@ -6,12 +6,14 @@ import ( "net/http" "time" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/middleware" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" ) // LoginHandler issues a JWT cookie func LoginHandler(w http.ResponseWriter, r *http.Request) { + middleware.HandleCORS(w, r) var creds security.UserCredentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { diff --git a/src/api/httpauth/logout.go b/src/api/httpauth/logout.go index e0764ae3..397555ea 100644 --- a/src/api/httpauth/logout.go +++ b/src/api/httpauth/logout.go @@ -5,9 +5,12 @@ import ( "net/http" "strings" "time" + + "github.com/SteamServerUI/SteamServerUI/v7/src/api/middleware" ) func LogoutHandler(w http.ResponseWriter, r *http.Request) { + middleware.HandleCORS(w, r) // Clear the cookie by setting it with an expired time http.SetCookie(w, &http.Cookie{ Name: "AuthToken", diff --git a/src/api/httpauth/registerapikey.go b/src/api/httpauth/registerapikey.go index 7fcb1b0a..e1e44fa8 100644 --- a/src/api/httpauth/registerapikey.go +++ b/src/api/httpauth/registerapikey.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/middleware" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" "github.com/SteamServerUI/SteamServerUI/v7/src/logger" @@ -12,6 +13,7 @@ import ( ) func RegisterAPIKeyHandler(w http.ResponseWriter, r *http.Request) { + middleware.HandleCORS(w, r) // Handle preflight OPTIONS requests if r.Method == http.MethodOptions { diff --git a/src/api/httpauth/registeruser.go b/src/api/httpauth/registeruser.go index fa2fe0d2..eb520c32 100644 --- a/src/api/httpauth/registeruser.go +++ b/src/api/httpauth/registeruser.go @@ -5,12 +5,14 @@ import ( "net/http" "strings" + "github.com/SteamServerUI/SteamServerUI/v7/src/api/middleware" "github.com/SteamServerUI/SteamServerUI/v7/src/config" "github.com/SteamServerUI/SteamServerUI/v7/src/core/security" ) // RegisterUserHandler registers new users func RegisterUserHandler(w http.ResponseWriter, r *http.Request) { + middleware.HandleCORS(w, r) // Handle preflight OPTIONS requests if r.Method == http.MethodOptions { diff --git a/src/api/middleware/authmiddleware.go b/src/api/middleware/authmiddleware.go index 144687d7..e9092491 100644 --- a/src/api/middleware/authmiddleware.go +++ b/src/api/middleware/authmiddleware.go @@ -19,6 +19,7 @@ func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Log request details for debugging //logger.Web.Debug("Request Path:" + r.URL.Path) //very spammy + HandleCORS(w, r) // Check for first-time setup redirect if config.GetIsFirstTimeSetup() { @@ -74,3 +75,25 @@ func AuthMiddleware(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +// Helper function to set CORS headers consistently across handlers +func SetCORSHeaders(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) // TODO: This NEEDS to change, as we allow any origin atm + } else { + w.Header().Set("Access-Control-Allow-Origin", "*") + } + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Expose-Headers", "Set-Cookie") +} + +func HandleCORS(w http.ResponseWriter, r *http.Request) { + SetCORSHeaders(w, r) + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } +} From a1e970cfff44ee734b7e2b9991114c546083a05f Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Mon, 20 Oct 2025 16:35:58 +0200 Subject: [PATCH 68/93] added firstpass on file gather/edit/save api --- src/api/routes.go | 5 + src/api/runfileapi/localfiles.go | 242 ++++++++++++++++++++++++ src/cli/runtimecommands.go | 5 + src/steamserverui/runfile/argexample.go | 12 ++ src/steamserverui/runfile/args.go | 40 +++- src/steamserverui/runfile/getters.go | 11 +- 6 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 src/api/runfileapi/localfiles.go diff --git a/src/api/routes.go b/src/api/routes.go index 48c90f73..cd46539a 100644 --- a/src/api/routes.go +++ b/src/api/routes.go @@ -116,6 +116,11 @@ func SetupAPIRoutes() (*http.ServeMux, *http.ServeMux) { protectedMux.HandleFunc("/api/v2/gallery", runfileapi.GalleryHandler) protectedMux.HandleFunc("/api/v2/gallery/select", runfileapi.GallerySelectHandler) + // --- FILE MANAGEMENT --- + protectedMux.HandleFunc("/api/v2/files", runfileapi.GetFileList) + protectedMux.HandleFunc("/api/v2/files/get", runfileapi.GetFile) + protectedMux.HandleFunc("/api/v2/files/save", runfileapi.SaveFile) + // --- PLUGINS --- protectedMux.HandleFunc("/api/v2/plugins/list", pluginsapi.HandleListPlugins) diff --git a/src/api/runfileapi/localfiles.go b/src/api/runfileapi/localfiles.go new file mode 100644 index 00000000..457e9326 --- /dev/null +++ b/src/api/runfileapi/localfiles.go @@ -0,0 +1,242 @@ +package runfileapi + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/SteamServerUI/SteamServerUI/v7/src/logger" + "github.com/SteamServerUI/SteamServerUI/v7/src/steamserverui/runfile" +) + +// FileRequest represents the JSON body for file operations +type FileRequest struct { + Filename string `json:"filename"` + Content string `json:"content,omitempty"` // Used for save operations +} + +// FileResponse represents the JSON response structure +type FileResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data any `json:"data,omitempty"` +} + +// GetFileList handles GET requests to list all available filenames +func GetFileList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + sendFileError(w, http.StatusMethodNotAllowed, "only GET requests are allowed") + return + } + + files := runfile.GetFiles() + if files == nil { + sendFileError(w, http.StatusNotFound, "no runfile loaded or no files available") + return + } + + filenames := make([]string, 0, len(files)) + for _, file := range files { + // if filename is inside the SSUI subdirectory, dont add file to the list + if strings.HasPrefix(file.Filepath, "./SSUI") { + continue + } + + // if file does not exist, dont add it to the list + if _, err := os.Stat(file.Filepath); os.IsNotExist(err) { + continue + } + filenames = append(filenames, file.Filename) + } + + sendFileResponse(w, http.StatusOK, FileResponse{ + Success: true, + Data: filenames, + }) +} + +// GetFile handles GET requests to retrieve a specific file's contents +func GetFile(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + sendFileError(w, http.StatusMethodNotAllowed, "only GET requests are allowed") + return + } + + var req FileRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + sendFileError(w, http.StatusBadRequest, fmt.Sprintf("failed to parse request JSON body: %v", err)) + return + } + + if req.Filename == "" { + sendFileError(w, http.StatusBadRequest, "filename is required") + return + } + + // Find the file in runfile + files := runfile.GetFiles() + var targetFile *runfile.File + for _, file := range files { + if file.Filename == req.Filename { + targetFile = &file + break + } + } + + if targetFile == nil { + sendFileError(w, http.StatusNotFound, fmt.Sprintf("file %s not found in runfile", req.Filename)) + return + } + + // Check if file is in SSUI subdirectory + if strings.HasPrefix(targetFile.Filepath, "./SSUI") { + sendFileError(w, http.StatusForbidden, fmt.Sprintf("access to a file %s in the SSUI subdirectory is forbidden", req.Filename)) + return + } + + // Stat the file + fileInfo, err := os.Stat(targetFile.Filepath) + if err != nil { + if os.IsNotExist(err) { + sendFileError(w, http.StatusNotFound, fmt.Sprintf("file %s does not exist at %s", req.Filename, targetFile.Filepath)) + } else { + sendFileError(w, http.StatusInternalServerError, fmt.Sprintf("failed to stat file %s: %v", req.Filename, err)) + } + return + } + + // Check if file is writable + if fileInfo.Mode().Perm()&0222 == 0 { + sendFileError(w, http.StatusForbidden, fmt.Sprintf("file %s is not writable", req.Filename)) + return + } + + // Read file contents + content, err := os.ReadFile(targetFile.Filepath) + if err != nil { + sendFileError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read file %s: %v", req.Filename, err)) + return + } + + // Set content type based on file type + contentType := "text/plain" + switch strings.ToLower(targetFile.Type) { + case "json": + contentType = "application/json" + case "xml": + contentType = "application/xml" + case "yaml": + contentType = "application/yaml" + case "ini": + contentType = "text/plain" + } + + // Send file contents directly + w.Header().Set("Content-Type", contentType) + w.WriteHeader(http.StatusOK) + if _, err := w.Write(content); err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to write file response for %s: %v", req.Filename, err)) + } +} + +// SaveFile handles POST requests to save an edited file +func SaveFile(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + sendFileError(w, http.StatusMethodNotAllowed, "only POST requests are allowed") + return + } + + // Get filename from query parameter + filename := r.URL.Query().Get("filename") + if filename == "" { + sendFileError(w, http.StatusBadRequest, "filename query parameter is required") + return + } + + // Read raw content from request body + content, err := io.ReadAll(r.Body) + if err != nil { + sendFileError(w, http.StatusBadRequest, fmt.Sprintf("failed to read request body: %v", err)) + return + } + if len(content) == 0 { + sendFileError(w, http.StatusBadRequest, "content is required") + return + } + + // Find the file in runfile + files := runfile.GetFiles() + var targetFile *runfile.File + for _, file := range files { + if file.Filename == filename { + targetFile = &file + break + } + } + + if targetFile == nil { + sendFileError(w, http.StatusNotFound, fmt.Sprintf("file %s not found in runfile", filename)) + return + } + + // Check if file is in SSUI subdirectory + if strings.HasPrefix(targetFile.Filepath, "./SSUI") { + sendFileError(w, http.StatusForbidden, fmt.Sprintf("saving file %s in the SSUI subdirectory is forbidden", filename)) + return + } + + // Stat the file + fileInfo, err := os.Stat(targetFile.Filepath) + if err != nil && !os.IsNotExist(err) { + sendFileError(w, http.StatusInternalServerError, fmt.Sprintf("failed to stat file %s: %v", filename, err)) + return + } + + // Check if file is writable (or would be writable if it exists) + if fileInfo != nil && fileInfo.Mode().Perm()&0222 == 0 { + sendFileError(w, http.StatusForbidden, fmt.Sprintf("file %s is not writable", filename)) + return + } + + // Write file with retries + const maxRetries = 3 + for attempt := 1; attempt <= maxRetries; attempt++ { + if err := os.WriteFile(targetFile.Filepath, content, 0644); err != nil { + logger.Runfile.Warn(fmt.Sprintf("failed to write file %s: attempt=%d, error=%v", filename, attempt, err)) + if attempt == maxRetries { + sendFileError(w, http.StatusInternalServerError, fmt.Sprintf("failed to write file %s after %d attempts: %v", filename, maxRetries, err)) + return + } + time.Sleep(100 * time.Millisecond) + continue + } + break + } + + sendFileResponse(w, http.StatusOK, FileResponse{ + Success: true, + Message: fmt.Sprintf("file %s saved successfully", filename), + }) +} + +// sendFileResponse sends a JSON response +func sendFileResponse(w http.ResponseWriter, status int, resp FileResponse) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(resp); err != nil { + logger.Runfile.Error(fmt.Sprintf("failed to encode response: %v", err)) + } +} + +// sendFileError sends an error response as JSON +func sendFileError(w http.ResponseWriter, status int, message string) { + logger.API.Debug(message) + sendFileResponse(w, status, FileResponse{ + Success: false, + Message: message, + }) +} diff --git a/src/cli/runtimecommands.go b/src/cli/runtimecommands.go index 2c10e93b..174f25f9 100644 --- a/src/cli/runtimecommands.go +++ b/src/cli/runtimecommands.go @@ -166,6 +166,7 @@ func init() { RegisterCommand("setdummybuildid", WrapNoReturn(setDummyBuildID), "sdbid") RegisterCommand("printconfig", WrapNoReturn(printConfig), "pc") RegisterCommand("testargbuilder", WrapNoReturn(TestArgBuilder), "targb") + RegisterCommand("testrunfilefiles", WrapNoReturn(TestRunfileFiles), "trff") } func startServer() { @@ -318,3 +319,7 @@ func supportPackage() { func TestArgBuilder() { runfile.TestArgBuilder() } + +func TestRunfileFiles() { + runfile.TestRunfileFiles() +} diff --git a/src/steamserverui/runfile/argexample.go b/src/steamserverui/runfile/argexample.go index 7d84a6d8..95907ad1 100644 --- a/src/steamserverui/runfile/argexample.go +++ b/src/steamserverui/runfile/argexample.go @@ -57,3 +57,15 @@ func TestArgBuilder() { } fmt.Println(args) } + +func TestRunfileFiles() { + files := GetFiles() + for _, file := range files { + // print all file details + fmt.Printf("File: %s\n", file.Filename) + fmt.Printf("Filepath: %s\n", file.Filepath) + fmt.Printf("Type: %s\n", file.Type) + fmt.Printf("Description: %s\n", file.Description) + fmt.Println() + } +} diff --git a/src/steamserverui/runfile/args.go b/src/steamserverui/runfile/args.go index d1f19bab..a1582d4f 100644 --- a/src/steamserverui/runfile/args.go +++ b/src/steamserverui/runfile/args.go @@ -67,6 +67,13 @@ type GameArg struct { Disabled bool `json:"disabled,omitempty"` } +type File struct { + Filename string `json:"filename"` + Filepath string `json:"filepath"` + Type string `json:"type"` + Description string `json:"description"` +} + type Meta struct { Name string `json:"name"` // SSUI Specific Game Identifier, must match the one in the filename. Version string `json:"version"` // Runfile version @@ -76,10 +83,11 @@ type RunFile struct { Meta Meta `json:"meta"` Architecture string `json:"architecture,omitempty"` SteamAppID string `json:"steam_app_id"` - SteamLoginRequired bool `json:"steam_login_required,omitempty"` //unused & unsupported, will later be used in combination with some way to provide a steam login + SteamLoginRequired bool `json:"steam_login_required,omitempty"` WindowsExecutable string `json:"windows_executable"` LinuxExecutable string `json:"linux_executable"` Args map[string][]GameArg `json:"args"` + Files []File `json:"files,omitempty"` } // Validate checks the RunFile state @@ -130,6 +138,34 @@ func (rf *RunFile) Validate() error { } } + // Validate files + for _, file := range rf.Files { + if file.Filename == "" { + issues = append(issues, "file name is required") + } + + if file.Filepath == "" { + issues = append(issues, "file path is required") + } + if file.Type == "" { + issues = append(issues, fmt.Sprintf("file type is required for %s", file.Filename)) + } else { + validTypes := map[string]bool{ + "json": true, + "ini": true, + "xml": true, + "yaml": true, + "text": true, + } + if !validTypes[file.Type] { + issues = append(issues, fmt.Sprintf("invalid file type %s for %s", file.Type, file.Filename)) + } + } + if file.Description == "" { + issues = append(issues, fmt.Sprintf("description is required for %s", file.Filename)) + } + } + if len(issues) > 0 { return ErrValidation{Issues: issues} } @@ -357,7 +393,7 @@ func BuildCommandArgs() ([]string, error) { }) for _, arg := range allArgs { - if arg.Disabled || (!arg.Required && arg.RequiresValue && arg.RuntimeValue == "") { //Clear text for clarity: skip if disabled OR if it's an optional argument that needs a value but doesn't have one set + if arg.Disabled || (!arg.Required && arg.RequiresValue && arg.RuntimeValue == "") { continue } diff --git a/src/steamserverui/runfile/getters.go b/src/steamserverui/runfile/getters.go index 580ef9e2..71293806 100644 --- a/src/steamserverui/runfile/getters.go +++ b/src/steamserverui/runfile/getters.go @@ -17,6 +17,16 @@ func GetAllArgs() []GameArg { return CurrentRunfile.getAllArgs() } +// GetFiles returns all Files from the runfile +func GetFiles() []File { + if CurrentRunfile == nil { + logger.Runfile.Error("runfile not loaded") + return nil + } + return CurrentRunfile.Files +} + +// GetUIGroups returns all unique UIGroup values from the runfile func GetUIGroups() []string { if CurrentRunfile == nil { logger.Runfile.Error("runfile not loaded") @@ -98,7 +108,6 @@ func (rf *RunFile) GetExecutable() (string, error) { } // If Architecture is all or empty, select based on GOOS - if goos == "windows" { if rf.WindowsExecutable == "" { return "", fmt.Errorf("WindowsExecutable is required for windows OS") From b77d5f8962e0085307911dce01660b1668da34cb Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 21 Oct 2025 02:36:32 +0200 Subject: [PATCH 69/93] added more info to the getFiles endpoint --- src/api/runfileapi/localfiles.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/api/runfileapi/localfiles.go b/src/api/runfileapi/localfiles.go index 457e9326..b2102cf3 100644 --- a/src/api/runfileapi/localfiles.go +++ b/src/api/runfileapi/localfiles.go @@ -26,7 +26,14 @@ type FileResponse struct { Data any `json:"data,omitempty"` } -// GetFileList handles GET requests to list all available filenames +// FileInfo represents the file information returned by GetFileList +type FileInfo struct { + Filename string `json:"filename"` + Type string `json:"type"` + Description string `json:"description"` +} + +// GetFileList handles GET requests to list all available files with their details func GetFileList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { sendFileError(w, http.StatusMethodNotAllowed, "only GET requests are allowed") @@ -39,7 +46,7 @@ func GetFileList(w http.ResponseWriter, r *http.Request) { return } - filenames := make([]string, 0, len(files)) + fileInfos := make([]FileInfo, 0, len(files)) for _, file := range files { // if filename is inside the SSUI subdirectory, dont add file to the list if strings.HasPrefix(file.Filepath, "./SSUI") { @@ -50,12 +57,17 @@ func GetFileList(w http.ResponseWriter, r *http.Request) { if _, err := os.Stat(file.Filepath); os.IsNotExist(err) { continue } - filenames = append(filenames, file.Filename) + + fileInfos = append(fileInfos, FileInfo{ + Filename: file.Filename, + Type: file.Type, + Description: file.Description, + }) } sendFileResponse(w, http.StatusOK, FileResponse{ Success: true, - Data: filenames, + Data: fileInfos, }) } From 3f46b8140002cef442e404852ee1ab69453fc065 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 21 Oct 2025 03:17:11 +0200 Subject: [PATCH 70/93] changed getFile endpoint to POST --- src/api/runfileapi/localfiles.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/runfileapi/localfiles.go b/src/api/runfileapi/localfiles.go index b2102cf3..4df82cb9 100644 --- a/src/api/runfileapi/localfiles.go +++ b/src/api/runfileapi/localfiles.go @@ -73,8 +73,8 @@ func GetFileList(w http.ResponseWriter, r *http.Request) { // GetFile handles GET requests to retrieve a specific file's contents func GetFile(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - sendFileError(w, http.StatusMethodNotAllowed, "only GET requests are allowed") + if r.Method != http.MethodPost { + sendFileError(w, http.StatusMethodNotAllowed, "only POST requests are allowed") return } From 40efcd191c4a6f223a275e6b20c662d7aabb5447 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 21 Oct 2025 03:17:53 +0200 Subject: [PATCH 71/93] added fileView to settings tab: allows editing additional config files directly from WebUI --- .../src/components/settings/FileView.svelte | 412 ++++++++++++++++++ .../components/settings/SettingsView.svelte | 6 + 2 files changed, 418 insertions(+) create mode 100644 frontend/src/components/settings/FileView.svelte diff --git a/frontend/src/components/settings/FileView.svelte b/frontend/src/components/settings/FileView.svelte new file mode 100644 index 00000000..9ae0055f --- /dev/null +++ b/frontend/src/components/settings/FileView.svelte @@ -0,0 +1,412 @@ + + +

+ {#if error} +
+ {error} +
+ {/if} + +
+ {#if !selectedFile} +
+
+

File Browser

+
+ + {#if loading && files.length === 0} +
Loading files...
+ {:else if files.length === 0} +
No files available
+ {:else} + + + + + + + + + + + {#each files as file} + + + + + + + {/each} + +
FilenameTypeDescriptionAction
{file.filename} + {file.type} + {file.description} + +
+ {/if} +
+ {:else} +
+
+
+ +

{selectedFile.filename}

+ {selectedFile.type} + {#if hasChanges} + • Unsaved changes + {/if} +
+ +
+ +
+ +
+
+ {/if} +
+
+ + \ No newline at end of file diff --git a/frontend/src/components/settings/SettingsView.svelte b/frontend/src/components/settings/SettingsView.svelte index bc3ea5b9..ad2a48d2 100644 --- a/frontend/src/components/settings/SettingsView.svelte +++ b/frontend/src/components/settings/SettingsView.svelte @@ -5,6 +5,7 @@ import BackendSettings from './BackendSettings.svelte'; import DetectionManager from './DetectionManager.svelte'; import ConfigManager from './ConfigManager.svelte'; + import FileView from './FileView.svelte'; // State management let activeSidebarTab = $state('SSUI Settings'); // Default to General tab in sidebar @@ -25,6 +26,9 @@ + @@ -40,6 +44,8 @@ {:else if activeSidebarTab === 'Backends'} + {:else if activeSidebarTab === 'Files'} + {:else if activeSidebarTab === 'Legacy Detection Manager'} {:else if activeSidebarTab === 'Legacy Config Manager'} From 2cd5d24c09e6c2673ff6fb5c3d2cf07b5398a421 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 21 Oct 2025 15:15:43 +0200 Subject: [PATCH 72/93] improved file view, now has structured view display and notifications --- .../src/components/settings/FileView.svelte | 624 ++++++++++++++++-- 1 file changed, 577 insertions(+), 47 deletions(-) diff --git a/frontend/src/components/settings/FileView.svelte b/frontend/src/components/settings/FileView.svelte index 9ae0055f..7767f4c8 100644 --- a/frontend/src/components/settings/FileView.svelte +++ b/frontend/src/components/settings/FileView.svelte @@ -4,17 +4,33 @@ let files = $state([]); let selectedFile = $state(null); + let editorMode = $state('raw'); // 'raw', 'json', 'ini', 'yaml' let fileContent = $state(''); let originalContent = $state(''); + let parsedContent = $state(null); let loading = $state(false); let error = $state(null); let saving = $state(false); + let notification = $state(null); let hasChanges = $derived(fileContent !== originalContent); onMount(async () => { await loadFileList(); }); + const maxNestingLevels = { + json: 2, + yaml: 2, + ini: 1 + }; + + function showNotification(message, type = 'info') { + notification = { message, type }; + setTimeout(() => { + notification = null; + }, 4000); + } + async function loadFileList() { loading = true; error = null; @@ -34,7 +50,18 @@ } } - async function selectFile(file) { + function checkDeepNesting(obj, fileType, currentDepth = 0) { + const maxDepth = maxNestingLevels[fileType.toLowerCase()] || 2; + if (currentDepth > maxDepth) return true; + if (typeof obj !== 'object' || obj === null) return false; + + return Object.values(obj).some(value => + typeof value === 'object' && value !== null && + checkDeepNesting(value, fileType, currentDepth + 1) + ); + } + + async function selectFile(file, mode = 'raw') { if (hasChanges && !confirm('You have unsaved changes. Are you sure you want to switch files?')) { return; } @@ -42,6 +69,7 @@ loading = true; error = null; selectedFile = file; + editorMode = mode; try { const response = await apiFetch('/api/v2/files/get', { @@ -49,19 +77,34 @@ body: JSON.stringify({ filename: file.filename }) }); - const contentType = response.headers.get('content-type'); - let content; - - if (contentType?.includes('application/json') || - contentType?.includes('application/xml') || - contentType?.includes('application/yaml')) { - content = await response.text(); - } else { - content = await response.text(); - } - + const content = await response.text(); fileContent = content; originalContent = content; + + if (mode !== 'raw') { + try { + let parsed; + if (mode === 'json') { + parsed = JSON.parse(content); + } else if (mode === 'yaml') { + parsed = parseYAML(content); + } else { + parsed = parseINI(content); + } + + if (checkDeepNesting(parsed, mode)) { + showNotification(`Cannot render ${mode.toUpperCase()} file in structured view due to deep nesting`, 'error'); + editorMode = 'raw'; + parsedContent = null; + } else { + parseContent(content, mode); + } + } catch (err) { + showNotification(`Failed to parse ${mode.toUpperCase()}: ${err.message}`, 'error'); + editorMode = 'raw'; + parsedContent = null; + } + } } catch (err) { error = 'Error loading file: ' + err.message; selectedFile = null; @@ -70,6 +113,127 @@ } } + function parseContent(content, mode) { + try { + if (mode === 'json') { + parsedContent = JSON.parse(content); + } else if (mode === 'ini') { + parsedContent = parseINI(content); + } else if (mode === 'yaml') { + parsedContent = parseYAML(content); + } + } catch (err) { + showNotification(`Failed to parse ${mode.toUpperCase()}: ${err.message}`, 'error'); + editorMode = 'raw'; + parsedContent = null; + } + } + + function parseINI(content) { + const lines = content.split('\n'); + const result = {}; + let currentSection = 'global'; + result[currentSection] = {}; + + for (let line of lines) { + line = line.trim(); + if (!line || line.startsWith(';') || line.startsWith('#')) continue; + + if (line.startsWith('[') && line.endsWith(']')) { + currentSection = line.slice(1, -1); + result[currentSection] = {}; + } else { + const idx = line.indexOf('='); + if (idx > 0) { + const key = line.slice(0, idx).trim(); + const value = line.slice(idx + 1).trim(); + result[currentSection][key] = value; + } + } + } + return result; + } + + function parseYAML(content) { + const lines = content.split('\n'); + const result = {}; + const stack = [{ obj: result, indent: -1 }]; + + for (let line of lines) { + if (!line.trim() || line.trim().startsWith('#')) continue; + + const indent = line.search(/\S/); + const trimmed = line.trim(); + + while (stack.length > 1 && indent <= stack[stack.length - 1].indent) { + stack.pop(); + } + + if (trimmed.includes(':')) { + const idx = trimmed.indexOf(':'); + const key = trimmed.slice(0, idx).trim(); + const value = trimmed.slice(idx + 1).trim(); + + if (value) { + stack[stack.length - 1].obj[key] = value; + } else { + const newObj = {}; + stack[stack.length - 1].obj[key] = newObj; + stack.push({ obj: newObj, indent }); + } + } + } + return result; + } + + function serializeINI(data) { + let result = ''; + for (const [section, values] of Object.entries(data)) { + if (section !== 'global' || Object.keys(values).length > 0) { + if (section !== 'global') { + result += `[${section}]\n`; + } + for (const [key, value] of Object.entries(values)) { + result += `${key}=${value}\n`; + } + result += '\n'; + } + } + return result.trim(); + } + + function serializeYAML(data, indent = 0) { + let result = ''; + for (const [key, value] of Object.entries(data)) { + const spaces = ' '.repeat(indent); + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + result += `${spaces}${key}:\n`; + result += serializeYAML(value, indent + 1); + } else { + result += `${spaces}${key}: ${value}\n`; + } + } + return result; + } + + function updateParsedValue(path, value) { + const keys = path.split('.'); + let obj = parsedContent; + + for (let i = 0; i < keys.length - 1; i++) { + obj = obj[keys[i]]; + } + obj[keys[keys.length - 1]] = value; + + if (editorMode === 'json') { + fileContent = JSON.stringify(parsedContent, null, 2); + } else if (editorMode === 'ini') { + fileContent = serializeINI(parsedContent); + } else if (editorMode === 'yaml') { + fileContent = serializeYAML(parsedContent); + } + } + async function saveFile() { if (!selectedFile) return; @@ -86,12 +250,14 @@ if (data.success) { originalContent = fileContent; - alert(data.message || 'File saved successfully'); + showNotification(data.message || 'File saved successfully', 'success'); } else { error = data.message || 'Failed to save file'; + showNotification(error, 'error'); } } catch (err) { error = 'Error saving file: ' + err.message; + showNotification(error, 'error'); } finally { saving = false; } @@ -104,10 +270,64 @@ selectedFile = null; fileContent = ''; originalContent = ''; + parsedContent = null; + editorMode = 'raw'; + } + + function switchMode(mode) { + if (mode === 'raw') { + editorMode = 'raw'; + parsedContent = null; + } else { + try { + let parsed; + if (mode === 'json') { + parsed = JSON.parse(fileContent); + } else if (mode === 'yaml') { + parsed = parseYAML(fileContent); + } else { + parsed = parseINI(fileContent); + } + + if (checkDeepNesting(parsed, mode)) { + showNotification(`Cannot render ${mode.toUpperCase()} file in structured view due to deep nesting`, 'error'); + editorMode = 'raw'; + parsedContent = null; + } else { + parseContent(fileContent, mode); + if (parsedContent) { + editorMode = mode; + } + } + } catch (err) { + showNotification(`Failed to parse ${mode.toUpperCase()}: ${err.message}`, 'error'); + editorMode = 'raw'; + parsedContent = null; + } + } + } + + function canUseStructuredEditor(file) { + return ['json', 'ini', 'yaml'].includes(file.type.toLowerCase()); + } + + function getEditorLabel(type) { + const labels = { + json: 'JSON Editor', + ini: 'INI Editor', + yaml: 'YAML Editor' + }; + return labels[type.toLowerCase()] || 'Editor'; }
+ {#if notification} +
+ {notification.message} +
+ {/if} + {#if error}
{error} @@ -132,25 +352,32 @@ Filename Type Description - Action + Actions {#each files as file} {file.filename} - - {file.type} - + {file.type} {file.description} - + + {#if canUseStructuredEditor(file)} + + {/if} {/each} @@ -164,27 +391,148 @@

{selectedFile.filename}

- {selectedFile.type} + {editorMode.toUpperCase()} {#if hasChanges} • Unsaved changes {/if}
- +
+ {#if canUseStructuredEditor(selectedFile)} +
+ + +
+ {/if} + +
- + {#if editorMode === 'raw'} + + {:else if editorMode === 'json' && parsedContent} +
+
JSON Structure
+
+ {#each Object.entries(parsedContent) as [key, value], i} +
+ + + {#if typeof value === 'object' && value !== null} +
+ {#each Object.entries(value) as [nestedKey, nestedValue], j} +
+ + + updateParsedValue(`${key}.${nestedKey}`, e.currentTarget.value)} + class="json-input" + /> +
+ {/each} +
+ {:else} + updateParsedValue(key, e.currentTarget.value)} + class="json-input" + /> + {/if} +
+ {/each} +
+
+ {:else if editorMode === 'ini' && parsedContent} +
+
INI Structure
+
+ {#each Object.entries(parsedContent) as [section, values], i} +
+ {#if section !== 'global'} +
[{section}]
+ {:else} +
Global Settings
+ {/if} +
+ {#each Object.entries(values) as [key, value], j} +
+ + + updateParsedValue(`${section}.${key}`, e.currentTarget.value)} + class="ini-input" + /> +
+ {/each} +
+
+ {/each} +
+
+ {:else if editorMode === 'yaml' && parsedContent} +
+
YAML Structure
+
+ {#each Object.entries(parsedContent) as [key, value], i} +
+ + + {#if typeof value === 'object' && value !== null} +
+ {#each Object.entries(value) as [nestedKey, nestedValue], j} +
+ + + updateParsedValue(`${key}.${nestedKey}`, e.currentTarget.value)} + class="yaml-input" + /> +
+ {/each} +
+ {:else} + updateParsedValue(key, e.currentTarget.value)} + class="yaml-input" + /> + {/if} +
+ {/each} +
+
+ {/if}
{/if} @@ -201,6 +549,45 @@ flex-direction: column; border-radius: 8px; box-shadow: var(--shadow-light); + position: relative; + } + + .notification { + position: absolute; + top: 80px; + right: 20px; + padding: 12px 20px; + border-radius: 6px; + font-weight: 500; + z-index: 1000; + box-shadow: var(--shadow-medium); + animation: slideIn 0.3s ease-out; + } + + @keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } + } + + .notification-success { + background: var(--accent-primary); + color: var(--bg-primary); + } + + .notification-error { + background: var(--text-warning); + color: var(--bg-primary); + } + + .notification-info { + background: var(--accent-secondary); + color: var(--bg-primary); } .error-banner { @@ -285,38 +672,50 @@ color: var(--text-accent); } - .type-badge { + .mode-badge { display: inline-block; padding: 4px 12px; background: var(--accent-tertiary); - color: var(--text-primary); + color: var(--text-secondary); border-radius: 12px; font-size: 12px; font-weight: 500; - text-transform: uppercase; } .description { color: var(--text-secondary); } - .open-btn { - padding: 6px 16px; - background: var(--accent-primary); - color: var(--bg-primary); + .action-btn { + padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; - font-size: 14px; + font-size: 13px; font-weight: 500; - transition: background 0.2s; + transition: all 0.2s; + } + + .raw-btn { + background: var(--bg-hover); + color: var(--text-primary); + border: 1px solid var(--border-color); + } + + .raw-btn:hover:not(:disabled) { + background: var(--bg-active); } - .open-btn:hover:not(:disabled) { + .structured-btn { + background: var(--accent-primary); + color: var(--bg-primary); + } + + .structured-btn:hover:not(:disabled) { background: var(--accent-secondary); } - .open-btn:disabled { + .action-btn:disabled { opacity: 0.5; cursor: not-allowed; } @@ -342,6 +741,39 @@ gap: 12px; } + .editor-actions { + display: flex; + align-items: center; + gap: 12px; + } + + .mode-switcher { + display: flex; + background: var(--bg-tertiary); + border-radius: 4px; + padding: 2px; + } + + .mode-btn { + padding: 6px 12px; + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + border-radius: 3px; + transition: all 0.2s; + } + + .mode-btn.active { + background: var(--accent-primary); + color: var(--bg-primary); + } + + .mode-btn:hover:not(.active) { + background: var(--bg-hover); + } + .back-btn { padding: 8px 12px; background: var(--bg-tertiary); @@ -387,7 +819,7 @@ .editor-content { flex: 1; padding: 20px; - overflow: hidden; + overflow: auto; } .editor { @@ -409,4 +841,102 @@ border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--shadow-light); } + + .structured-editor { + height: 100%; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: auto; + } + + .structured-header { + padding: 16px; + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); + } + + .json-tree, .yaml-editor { + padding: 16px; + } + + .json-item, .yaml-item { + margin-bottom: 16px; + } + + .json-item.nested, .yaml-item.nested { + margin-left: 24px; + margin-bottom: 12px; + } + + .json-key, .yaml-key { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: var(--text-accent); + font-size: 13px; + } + + .json-input, .yaml-input, .ini-input { + width: 100%; + padding: 8px 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-primary); + font-family: monospace; + font-size: 13px; + } + + .json-input:focus, .yaml-input:focus, .ini-input:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px var(--shadow-light); + } + + .json-nested, .yaml-nested { + margin-top: 12px; + padding-left: 16px; + border-left: 2px solid var(--border-color); + } + + .ini-editor { + padding: 16px; + } + + .ini-section { + margin-bottom: 24px; + } + + .section-header { + padding: 8px 12px; + background: var(--bg-tertiary); + border-radius: 4px; + font-weight: 600; + color: var(--text-accent); + margin-bottom: 12px; + font-family: monospace; + } + + .section-content { + padding-left: 12px; + } + + .ini-item { + margin-bottom: 12px; + } + + .ini-key { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: var(--text-secondary); + font-size: 13px; + font-family: monospace; + } \ No newline at end of file From 5aa484160ae8f66e00181326b03ee198798f6645 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 21 Oct 2025 15:21:14 +0200 Subject: [PATCH 73/93] ignored some svelte warnings and removed unused css --- frontend/src/Login.svelte | 5 ++++ frontend/src/components/nav/TopNav.svelte | 4 +++ frontend/src/components/views/LogsView.svelte | 2 ++ .../views/RunfileGalleryView.svelte | 26 ------------------- 4 files changed, 11 insertions(+), 26 deletions(-) diff --git a/frontend/src/Login.svelte b/frontend/src/Login.svelte index d0c3afb3..eb8328de 100644 --- a/frontend/src/Login.svelte +++ b/frontend/src/Login.svelte @@ -228,7 +228,10 @@ async function checkBackendStatus(id) {