445 lines
10 KiB
Go
445 lines
10 KiB
Go
//
|
|
// 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
|
|
}
|