From d28a4f02f9d2e8fbcf88b7cea3be70b14286df4b Mon Sep 17 00:00:00 2001 From: katamaz Date: Fri, 22 May 2026 18:22:43 +0300 Subject: [PATCH] first version --- .gitignore | 3 + README.md | 136 +++++++++++++++- appmodules.go | 182 +++++++++++++++++++++ go.mod | 13 ++ go.sum | 12 ++ logger.go | 52 ++++++ main.go | 413 +++++++++++++++++++++++++++++++++++++++++++++++ scriptsRunner.go | 46 ++++++ utils.go | 137 ++++++++++++++++ 9 files changed, 993 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 appmodules.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logger.go create mode 100644 main.go create mode 100644 scriptsRunner.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c55ad4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config/* +runners/* +output/* \ No newline at end of file diff --git a/README.md b/README.md index 4b4bfe3..a045048 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,137 @@ # dengitool -horrible scripting tool for microslop bindows \ No newline at end of file +simple but horrible scripting tool focused on microslop bindows servers written in go + +--- + +## example instalation + +> `dtool.exe` is portable, you can _use any directory_ + +1. Create folder and copy `dtool.exe`. For example in `C:\dtooll` +1. Add to env **`PATH`** your folder +1. That's it! Open a terminal and try to run `dtool -h` + +## setup app + +example tree + +``` +config +└─── + └─── + └───run + customscript.ps1 + check.ps1 + config.yml + fix.ps1 + restart.ps1 + start.ps1 + stop.ps1 +``` + +> **TIP:** you can leave only scripts that you need + +you can write scripts **in any language you want** which is supported by [built-in script runners](#built-in), OR you can write **your own** [custom runner](#custom-runners) + +#### check + +for checkking status you can use either your script or you can write in config.yml `check-url` [(details)](#config) + +#### restart + +you can define restart script or it would use start and stop + +#### config + +currently there are config for `application`, example: + +```yaml +name: my-application +``` + +and for `module` + +```yaml +name: my-module +# +check-url: https://example.com +# or +check-url: https://example.com 200 +# or even +check-url: https://example.com 200 Test%20Body%20Text +# or don't define at all +# +``` + +for now that's it. + +## usage + +Usage: + +``` +dtool [application] [action] [module] [flags] +``` + +Flags: + +``` + -h, --help help for dtool + --list list configured applications and modules + --list-runners list configured script runners + --silent suppress all output + --verbose enable debug output +``` + +you can use `everything` for `application` + +just `dtool` is alias for `dtool everything check` + +#### actions + +| | | +| ------------------------------ | ------------------------------------------------------ | +| `check/fix/restart/start/stop` | runs it's script [(or check have url defined)](#check) | +| `run [script name]` | runs script in `run` subdirectory | + +## runners + +dengitool automaticly detects available script extension + +#### built-in + +| name | extension | +| ------------------ | --------- | +| Windows Executable | `.exe` | +| PowerShell Script | `.ps1` | +| Python | `.py` | +| CMD Batch File | `.bat` | +| NodeJS | `.js` | + +#### custom runners + +you can create your own runners + +for this, you need to create a .yml file at folder `runners` near dtool.exe + +you can **override** script for built-in extension + +**here is an example:** + +```yaml +# runners/bun.yml +name: My Bun Runner +extension: .ts +runner: bun run %v +``` + +> `%v` - absolute script path + +now you can see it in `dtool --list-runners` + +--- + +# thanks + +![insert here meme](https://assets.ktkz.ru/meme/photo_2026-05-22_11-26-14.jpg) diff --git a/appmodules.go b/appmodules.go new file mode 100644 index 0000000..d2a419e --- /dev/null +++ b/appmodules.go @@ -0,0 +1,182 @@ +package main + +import ( + "errors" + "fmt" + "net/url" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +type AppModule struct { + Name string + Folder string + CheckS string +} + +type AppModuleConfig struct { + Name string `yaml:"name"` + CheckUrl *string `yaml:"check-url"` +} + +type Script struct { + Name string + AbsPath string + Runner ScriptRunner +} + +func (s *Script) Run() (string, error) { + args := runnerArgs(s.Runner.Runner, s.AbsPath) + if len(args) == 0 { + return "", errors.New("runner command is empty") + } + + cmd := exec.Command(args[0], args[1:]...) + output, err := cmd.CombinedOutput() + return string(output), err +} + +func runnerArgs(template string, scriptPath string) []string { + fields := strings.Fields(template) + for i, field := range fields { + fields[i] = strings.ReplaceAll(field, "%v", scriptPath) + } + return fields +} + +func (m *AppModule) HasScript(name string, subfolder *string) (bool, *Script) { + folder := m.Folder + if subfolder != nil { + folder = filepath.Join(folder, *subfolder) + } + + for _, r := range runners { + ext := r.Extension + if ext != "" && !strings.HasPrefix(ext, ".") { + ext = "." + ext + } + + scriptPath := filepath.Join(folder, name+ext) + if fileExists(scriptPath) { + abs, err := filepath.Abs(scriptPath) + if err != nil { + return false, nil + } + return true, &Script{Name: name, AbsPath: abs, Runner: r} + } + } + return false, nil +} +func (m *AppModule) Check() (bool, string, error) { + if strings.HasPrefix(m.CheckS, "URL ") { + check := strings.Split(m.CheckS, " ")[1:] + if len(check) < 1 || len(check) > 3 { + return false, "", errors.New("check url is bad") + } + checkUrl := check[0] + code := "" + if len(check) > 1 { + code = check[1] + } + body := "" + if len(check) > 2 { + dBody, err := url.QueryUnescape(check[2]) + if err != nil { + return false, "", errors.New("check body decode failed") + } + body = dBody + } + + cCode, cBody, err := fetch(checkUrl, defaultFetchTimeout) + if err != nil { + return false, "", err + } + if code != "" && strconv.Itoa(cCode) != code { + return false, "", fmt.Errorf("code is different: expected %s, got %d", code, cCode) + } + if body != "" && body != cBody { + return false, "", errors.New("body is different") + } + + return true, "", nil + + } else { + exists, script := m.HasScript("check", nil) + if exists && script != nil { + output, err := script.Run() + if err != nil { + return false, output, err + } else { + return true, output, nil + } + } else { + return false, "", errors.New("check script not found") + } + } +} + +func (m *AppModule) Start() (string, error) { + exists, script := m.HasScript("start", nil) + if exists && script != nil { + return script.Run() + } else { + return "", errors.New("start script not found") + } +} + +func (m *AppModule) Stop() (string, error) { + exists, script := m.HasScript("stop", nil) + if exists && script != nil { + return script.Run() + } else { + return "", errors.New("stop script not found") + } +} + +func (m *AppModule) Restart() (string, error) { + exists, script := m.HasScript("restart", nil) + if exists && script != nil { + return script.Run() + } else { + stopOutput, err := m.Stop() + if err != nil { + return stopOutput, err + } + startOutput, err := m.Start() + return joinOutput(stopOutput, startOutput), err + } + +} + +func (m *AppModule) Fix() (string, error) { + exists, script := m.HasScript("fix", nil) + + if exists && script != nil { + return script.Run() + } else { + return m.Restart() + } +} + +func (m *AppModule) Run(scriptN string) (string, error) { + var sub = "run" + exists, script := m.HasScript(scriptN, &sub) + + if exists && script != nil { + return script.Run() + } else { + return "", errors.New("run script not found") + } +} + +type Application struct { + Name string + Folder string + AppModules []AppModule +} + +type AppConfig struct { + Name string `yaml:"name"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7f4f410 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.ktkz.ru/katamaz/dengitool + +go 1.26.3 + +require ( + github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ff4d6ec --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..69b9091 --- /dev/null +++ b/logger.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "os" +) + +var ( + silentMode bool + verboseMode bool +) + +const ( + colorRed = "\x1b[31m" + colorReset = "\x1b[0m" +) + +type logger struct{} + +func (logger) Info(args ...any) { + if silentMode { + return + } + fmt.Fprintln(os.Stdout, args...) +} + +func (logger) Debug(args ...any) { + if silentMode || !verboseMode { + return + } + fmt.Fprintln(os.Stdout, args...) +} + +func (logger) Println(args ...any) { + if silentMode { + return + } + fmt.Fprintln(os.Stdout, args...) +} + +func (logger) Error(args ...any) { + fmt.Fprint(os.Stderr, colorRed) + fmt.Fprintln(os.Stderr, args...) + fmt.Fprint(os.Stderr, colorReset) +} + +func (logger) Fatalln(args ...any) { + log.Error(args...) + os.Exit(1) +} + +var log = logger{} diff --git a/main.go b/main.go new file mode 100644 index 0000000..11a9ff2 --- /dev/null +++ b/main.go @@ -0,0 +1,413 @@ +// +// project dengitool +// Copyright (C) 2026 katamaz +// + +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var runners = GetScriptsRunners() + +var silent, verbose, listModules, listRunners bool + +func init() { + rootCmd.PersistentFlags().BoolVar(&silent, "silent", false, "suppress all output") + rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "enable debug output") + rootCmd.PersistentFlags().BoolVar(&listModules, "list", false, "list configured applications and modules") + rootCmd.PersistentFlags().BoolVar(&listRunners, "list-runners", false, "list configured script runners") + + rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + // silent wins + initLogger(silent, verbose && !silent) + } + + if !(folderExists(("./config/"))) { + log.Info("No config folder found, creating new...") + if err := os.Mkdir("./config/", 0750); err != nil { + log.Fatalln("Failed to create config folder") + } + } +} + +func initLogger(silent bool, verbose bool) { + silentMode = silent + verboseMode = verbose +} + +var rootCmd = &cobra.Command{ + Use: "dtool [application] [action] [module]", + Short: "DengiTool", + Long: "DengiTool CLI", + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runCLI(args) + }, +} + +func main() { + if err := rootCmd.Execute(); err != nil { + log.Error(err) + os.Exit(1) + } +} + +func runCLI(args []string) error { + if listModules { + return printModules() + } + if listRunners { + return printRunners() + } + + request, err := parseRequest(args) + if err != nil { + return err + } + + apps := listApplications() + if len(apps) == 0 { + log.Println("No apps found\nNothing to do") + return nil + } + + targets, err := resolveTargets(apps, request) + if err != nil { + return err + } + + if len(targets) == 0 { + return errors.New("no modules matched") + } + + var results []actionResult + currentApp := "" + for i, target := range targets { + if target.app.Folder != currentApp { + currentApp = target.app.Folder + if !silent { + log.Println("▸ " + displayName(target.app.Name, filepath.Base(target.app.Folder))) + } + } + results = append(results, runModuleAction(target.app, target.module, request, treePrefix(i, targets))) + } + + showSummary(results) + if failed := countFailed(results); failed > 0 { + return fmt.Errorf("%d of %d action(s) failed", failed, len(results)) + } + + return nil +} + +func printModules() error { + apps := listApplications() + if len(apps) == 0 { + log.Println("No apps found") + return nil + } + + for _, app := range apps { + log.Println(displayName(app.Name, filepath.Base(app.Folder))) + if len(app.AppModules) == 0 { + log.Println(" (no modules)") + continue + } + + for _, module := range app.AppModules { + log.Println(" " + displayName(module.Name, filepath.Base(module.Folder))) + } + } + + return nil +} + +func printRunners() error { + if len(runners) == 0 { + log.Println("No runners found") + return nil + } + + for _, runner := range runners { + log.Println(fmt.Sprintf("%s %s", runner.Name, runner.Extension)) + if verbose { + log.Println(" " + runner.Runner) + } + } + + return nil +} + +type cliRequest struct { + application string + action string + module string + script string +} + +type moduleTarget struct { + app Application + module AppModule +} + +type actionResult struct { + label string + output string + err error +} + +func parseRequest(args []string) (cliRequest, error) { + request := cliRequest{application: "everything", action: "check"} + if len(args) == 0 { + return request, nil + } + + if len(args) > 4 { + return request, usageError() + } + + request.application = args[0] + if len(args) > 1 { + request.action = args[1] + } + + if !validAction(request.action) { + return request, fmt.Errorf("unknown action %q; valid actions: %s", request.action, strings.Join(validActions(), ", ")) + } + + if request.action == "run" { + if len(args) < 3 { + return request, errors.New("usage: dtool [application] run [script] [module]") + } + request.script = args[2] + if len(args) > 3 { + request.module = args[3] + } + return request, nil + } + + if len(args) > 2 { + request.module = args[2] + } + + return request, nil +} + +func validAction(action string) bool { + for _, valid := range validActions() { + if action == valid { + return true + } + } + return false +} + +func validActions() []string { + return []string{"check", "start", "stop", "restart", "fix", "run"} +} + +func usageError() error { + return errors.New("usage: dtool [application] [action] [module]\n dtool [application] run [script] [module]") +} + +func resolveTargets(apps []Application, request cliRequest) ([]moduleTarget, error) { + var selected []Application + if request.application == "everything" { + if request.module != "" { + return nil, errors.New(`"everything" runs across all modules; omit the module argument`) + } + selected = apps + } else { + app, ok := findApplication(apps, request.application) + if !ok { + return nil, fmt.Errorf("application %q not found; available applications: %s", request.application, applicationNames(apps)) + } + selected = []Application{app} + } + + var targets []moduleTarget + for _, app := range selected { + if request.module == "" { + for _, module := range app.AppModules { + targets = append(targets, moduleTarget{app: app, module: module}) + } + continue + } + + module, ok := findModule(app, request.module) + if !ok { + return nil, fmt.Errorf("module %q not found in %q; available modules: %s", request.module, app.Name, moduleNames(app)) + } + targets = append(targets, moduleTarget{app: app, module: module}) + } + + return targets, nil +} + +func findApplication(apps []Application, name string) (Application, bool) { + for _, app := range apps { + if sameName(app.Name, name) || sameName(filepath.Base(app.Folder), name) { + return app, true + } + } + return Application{}, false +} + +func findModule(app Application, name string) (AppModule, bool) { + for _, module := range app.AppModules { + if sameName(module.Name, name) || sameName(filepath.Base(module.Folder), name) { + return module, true + } + } + return AppModule{}, false +} + +func sameName(left string, right string) bool { + return strings.EqualFold(strings.TrimSpace(left), strings.TrimSpace(right)) +} + +func applicationNames(apps []Application) string { + names := make([]string, 0, len(apps)) + for _, app := range apps { + names = append(names, displayName(app.Name, filepath.Base(app.Folder))) + } + return strings.Join(names, ", ") +} + +func moduleNames(app Application) string { + names := make([]string, 0, len(app.AppModules)) + for _, module := range app.AppModules { + names = append(names, displayName(module.Name, filepath.Base(module.Folder))) + } + if len(names) == 0 { + return "(none)" + } + return strings.Join(names, ", ") +} + +func displayName(name string, folder string) string { + if sameName(name, folder) { + return name + } + return fmt.Sprintf("%s (%s)", name, folder) +} + +func runModuleAction(app Application, module AppModule, request cliRequest, prefix treeParts) actionResult { + label := fmt.Sprintf("%s/%s %s", app.Name, module.Name, request.action) + moduleLabel := fmt.Sprintf("%s %s", displayName(module.Name, filepath.Base(module.Folder)), request.action) + if request.action == "run" { + label = fmt.Sprintf("%s/%s run %s", app.Name, module.Name, request.script) + moduleLabel = fmt.Sprintf("%s run %s", displayName(module.Name, filepath.Base(module.Folder)), request.script) + } + + if !silent { + log.Println(prefix.branch + "⏳ " + moduleLabel) + } + + var err error + output := "" + switch request.action { + case "check": + _, output, err = module.Check() + case "start": + output, err = module.Start() + case "stop": + output, err = module.Stop() + case "restart": + output, err = module.Restart() + case "fix": + output, err = module.Fix() + case "run": + output, err = module.Run(request.script) + default: + err = fmt.Errorf("unknown action %q", request.action) + } + + if err != nil { + if !silent { + printTreeError(prefix.branch + "✗ " + moduleLabel + ": " + err.Error()) + printScriptOutput(prefix.continuation, output) + } + return actionResult{label: label, output: output, err: err} + } + + if !silent { + log.Println(prefix.branch + "✓ " + moduleLabel) + printScriptOutput(prefix.continuation, output) + } + return actionResult{label: label, output: output} +} + +type treeParts struct { + branch string + continuation string +} + +func treePrefix(index int, targets []moduleTarget) treeParts { + if index == len(targets)-1 || targets[index+1].app.Folder != targets[index].app.Folder { + return treeParts{branch: "└─ ", continuation: " "} + } + return treeParts{branch: "├─ ", continuation: "│ "} +} + +func printScriptOutput(prefix string, output string) { + output = strings.TrimRight(output, "\r\n") + if output == "" { + return + } + + for _, line := range strings.Split(output, "\n") { + log.Println(prefix + " " + strings.TrimRight(line, "\r")) + } +} + +func printTreeError(message string) { + fmt.Fprint(os.Stdout, colorRed) + fmt.Fprintln(os.Stdout, message) + fmt.Fprint(os.Stdout, colorReset) +} + +func showSummary(results []actionResult) { + if silent { + for _, result := range results { + if result.err != nil { + log.Error("FAIL", result.label+":", result.err) + printScriptOutput("", result.output) + } + } + return + } + + failed := countFailed(results) + if failed == 0 { + log.Println(fmt.Sprintf("Done: %d action(s) succeeded", len(results))) + return + } + + for _, result := range results { + if result.err != nil { + log.Debug("FAIL", result.label+":", result.err) + } + } + log.Error(fmt.Sprintf("Done: %d succeeded, %d failed", len(results)-failed, failed)) +} + +func countFailed(results []actionResult) int { + failed := 0 + for _, result := range results { + if result.err != nil { + failed++ + } + } + return failed +} diff --git a/scriptsRunner.go b/scriptsRunner.go new file mode 100644 index 0000000..4266799 --- /dev/null +++ b/scriptsRunner.go @@ -0,0 +1,46 @@ +package main + +import ( + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +type ScriptRunner struct { + Name string `yaml:"name"` + Extension string `yaml:"extension"` + Runner string `yaml:"runner"` +} + +func GetScriptsRunners() []ScriptRunner { + runners := []ScriptRunner{} + + if folderExists("./runners") { + entries, err := os.ReadDir("./runners/") + if err == nil { + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".yml") { + runner, err := os.ReadFile("./runners/" + e.Name()) + if err == nil { + var rnr ScriptRunner + if err := yaml.Unmarshal(runner, &rnr); err == nil { + runners = append(runners, rnr) + } + } + } + } + } + } + + defaultRunners := []ScriptRunner{ + {Name: "Windows Executable", Extension: ".exe", Runner: "%v"}, + {Name: "PowerShell Script", Extension: ".ps1", Runner: "powershell -NoProfile -ExecutionPolicy Bypass -File %v"}, + {Name: "Python", Extension: ".py", Runner: "python %v"}, + {Name: "CMD Batch File", Extension: ".bat", Runner: "%v"}, + {Name: "NodeJS", Extension: ".js", Runner: "node %v"}, + } + runners = append(runners, defaultRunners...) + + return runners +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..7bbd14e --- /dev/null +++ b/utils.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +const defaultFetchTimeout = 10 * time.Second + +func folderExists(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return info.IsDir() +} +func fileExists(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return !info.IsDir() +} + +func quoteCommandArg(arg string) string { + if runtime.GOOS == "windows" { + return `"` + strings.ReplaceAll(arg, `"`, `""`) + `"` + } + return `'` + strings.ReplaceAll(arg, `'`, `'\''`) + `'` +} + +func joinOutput(outputs ...string) string { + var parts []string + for _, output := range outputs { + output = strings.TrimRight(output, "\r\n") + if output != "" { + parts = append(parts, output) + } + } + return strings.Join(parts, "\n") +} + +func readAppModule(app, module string) AppModule { + path := filepath.Join(".", "config", app, module) + name := module + check := "SCRIPT" + configPath := filepath.Join(path, "config.yml") + if fileExists(configPath) { + configF, err := os.ReadFile(configPath) + if err == nil { + var cfg AppModuleConfig + if err := yaml.Unmarshal(configF, &cfg); err == nil { + if cfg.Name != "" { + name = cfg.Name + } + if cfg.CheckUrl != nil { + check = "URL " + *cfg.CheckUrl + } + } + } + } + + return AppModule{Name: name, Folder: path, CheckS: check} +} + +func readApplication(folderName string) Application { + name := folderName + appFolder := filepath.Join(".", "config", folderName) + configPath := filepath.Join(appFolder, "config.yml") + if fileExists(configPath) { + configF, err := os.ReadFile(configPath) + if err == nil { + var cfg AppConfig + if err := yaml.Unmarshal(configF, &cfg); err == nil { + if cfg.Name != "" { + name = cfg.Name + } + } + } + } + entries, err := os.ReadDir(appFolder) + if err != nil { + log.Fatalln(err) + } + var i []AppModule + for _, e := range entries { + if e.IsDir() { + i = append(i, readAppModule(folderName, e.Name())) + } + } + return Application{Name: name, Folder: appFolder, AppModules: i} +} + +func listApplications() []Application { + entries, err := os.ReadDir("./config") + if err != nil { + log.Fatalln(err) + } + var i []Application + for _, e := range entries { + if e.IsDir() { + i = append(i, readApplication(e.Name())) + } + } + return i +} + +func fetch(url string, timeout time.Duration) (int, string, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return 0, "", err + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return 0, "", err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, "", err + } + return resp.StatusCode, string(b), nil +}