// // project dengitool // Copyright (C) 2026 katamaz // package main import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" ) var baseDir string var runners []ScriptRunner var silent, verbose, listModules, listRunners bool func init() { var err error baseDir, err = exeDir() if err != nil { log.Error("Unable to determine executable folder, falling back to current directory:", err) baseDir = "." } 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) } configPath := filepath.Join(baseDir, "config") if !folderExists(configPath) { log.Info("No config folder found, creating new...") if err := os.Mkdir(configPath, 0750); err != nil { log.Fatalln("Failed to create config folder") } } runners = GetScriptsRunners() } func initLogger(silent bool, verbose bool) { silentMode = silent verboseMode = verbose } func exeDir() (string, error) { exe, err := os.Executable() if err != nil { return "", err } exe, err = filepath.EvalSymlinks(exe) if err != nil { return "", err } return filepath.Dir(exe), nil } func configFolder() string { return filepath.Join(baseDir, "config") } func runnersFolder() string { return filepath.Join(baseDir, "runners") } 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 }