Browse Source

feat: release v1.1.0 - visual hierarchy, task timers & KISS refactor

michelphp 1 week ago
parent
commit
c06d94072a
1 changed files with 483 additions and 493 deletions
  1. 483 493
      main.go

+ 483 - 493
main.go

@@ -1,493 +1,483 @@
-//
-//
-// @Author: F.Michel
-
-// @github: https://github.com/phpdevcommunity
-package main
-
-import (
-	"bufio"
-	"bytes"
-	"crypto/rand"
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strconv"
-	"strings"
-
-	"github.com/pterm/pterm"
-	"github.com/urfave/cli/v2"
-)
-
-var (
-	workflows            []Workflow
-	dockerComposeCommand = ""
-)
-
-func main() {
-
-	currentDir := getCurrentDir()
-	wfFiles, err := getWfFiles(currentDir)
-	if err != nil {
-		fmt.Println(err)
-	}
-
-	if len(wfFiles) == 0 {
-		fmt.Println("No workflow files found")
-	}
-
-	workflows = make([]Workflow, 0)
-	for _, wfFile := range wfFiles {
-
-		wfs := ParseContentToWorkFlowStruct(wfFile)
-		for _, wf := range wfs {
-			if len(wf.Lines) == 0 {
-				continue
-			}
-			workflows = append(workflows, wf)
-		}
-	}
-
-	Commands := []*cli.Command{}
-	values := InitDefaultVariables()
-	for _, wf := range workflows {
-
-		Commands = append(Commands, &cli.Command{
-			Name:  wf.Name,
-			Usage: wf.Comment,
-			Action: func(c *cli.Context) error {
-				executeWorkflow(wf, values)
-				return nil
-			},
-		})
-	}
-
-	app := &cli.App{
-		Commands: Commands,
-	}
-
-	if err := app.Run(os.Args); err != nil {
-		log.Fatal(err)
-	}
-
-}
-func executeWorkflow(wf Workflow, values *map[string]string) {
-	for _, line := range wf.Lines {
-		executeLine(line, values)
-	}
-}
-
-func FileExists(filename string) bool {
-	_, err := os.Stat(filename)
-	if err != nil && os.IsNotExist(err) {
-		return false
-	}
-	return true
-}
-
-func Touch(filename string) (bool, error) {
-	fd, err := os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0666)
-	if err != nil {
-		return false, err
-	}
-	fd.Close()
-	return true, nil
-}
-
-func executeLine(lineOriginal string, values *map[string]string) {
-
-	if lineOriginal == "" || strings.HasPrefix(lineOriginal, "#") {
-		return
-	}
-
-	lineOriginal = ResolveVariables(*values, lineOriginal)
-	actionOriginal := strings.Split(lineOriginal, " ")[0]
-	action := strings.ToLower(actionOriginal)
-	line := strings.Replace(lineOriginal, actionOriginal, action, 1)
-	if action == "set" {
-		line = strings.TrimPrefix(line, "set ")
-		parts := strings.Split(line, "=")
-		if len(parts) < 2 {
-			pterm.Error.WithFatal().Println("Invalid set command")
-		}
-		if parts[0] == "" || parts[1] == "" {
-			pterm.Error.WithFatal().Println("Invalid set command")
-		}
-
-		(*values)[parts[0]] = parts[1]
-		pterm.Info.Printfln("Variable %s set to %s", parts[0], (*values)[parts[0]])
-
-		return
-	} else if strings.HasPrefix(line, "#") {
-		return
-	} else if action == "run" {
-		line = strings.TrimPrefix(line, "run ")
-		run(line, true)
-		return
-	} else if action == "echo" {
-		line = strings.TrimPrefix(line, "echo ")
-		fmt.Println(line)
-		return
-	} else if action == "exit" {
-		os.Exit(0)
-	} else if action == "touch" {
-		line = strings.TrimPrefix(line, "touch ")
-		fileToCreate := strings.TrimSpace(line)
-		if fileToCreate == "" {
-			pterm.Error.WithFatal().Println("Invalid touch command")
-		}
-		if FileExists(fileToCreate) {
-			pterm.Info.Printfln("%s already exists, skipping...", fileToCreate)
-			return
-		}
-		Touch(line)
-		fmt.Println(pterm.FgGreen.Sprintf("%s created", line))
-		return
-	} else if action == "copy" || action == "cp" {
-		line = strings.TrimPrefix(line, "copy ")
-		parts := strings.Split(line, " ")
-
-		if len(parts) < 2 {
-			pterm.Error.WithFatal().Println("Invalid copy command")
-		}
-		fileOrFolder := parts[0]
-		destination := parts[1]
-		if !FileExists(fileOrFolder) {
-			pterm.Error.WithFatal().Printfln("%s not found", fileOrFolder)
-		}
-
-		if FileExists(destination) {
-			pterm.Info.Printfln("%s already exists, skipping...", destination)
-			return
-		}
-
-		Copy(fileOrFolder, destination)
-		fmt.Println(pterm.FgGreen.Sprintf("%s copied to %s", fileOrFolder, destination))
-		return
-
-	} else if action == "mkdir" {
-		folderName := strings.Split(line, " ")[1]
-		if folderName == "" {
-			pterm.Error.WithFatal().Println("Invalid mkdir command")
-		}
-
-		if FileExists(folderName) {
-			pterm.Info.Printfln("Folder %s already exists, skipping...", folderName)
-			return
-		}
-
-		err := os.MkdirAll(folderName, os.ModePerm)
-		if err != nil {
-			pterm.Error.WithFatal().Println(err)
-		}
-
-		fmt.Println(pterm.FgGreen.Sprintf("Folder %s created", folderName))
-		return
-	} else if action == "set_permissions" {
-		args := strings.Fields(line)
-		if len(args) < 3 {
-			pterm.Error.WithFatal().Printf("Invalid set_permissions command: %s", line)
-		}
-
-		folderName := args[1]
-		permissions, err := strconv.ParseUint(args[2], 8, 32) // Use ParseUint instead of Atoi
-		if err != nil {
-			pterm.Error.WithFatal().Printf("Invalid permissions: %s", args[2])
-		}
-
-		err = os.Chmod(folderName, os.FileMode(permissions))
-		if err != nil {
-			pterm.Error.WithFatal().Printf("Failed to set permissions for folder/file %s: %s", folderName, err)
-		}
-
-		fmt.Println(pterm.FgGreen.Sprintf("Permissions 0%o set for folder/file %s", permissions, folderName))
-		return
-	} else if action == "sync_time" {
-		fmt.Println("Checking time...")
-		run("date", false)
-		return
-	} else if action == "docker_compose" {
-		args := strings.Fields(line)
-		if len(args) < 2 {
-			pterm.Error.WithFatal().Printf("Invalid execute command: %s", line)
-		}
-		dockerComposeExec := args[1:]
-		dockerCompose := GetDockerComposeCommand()
-		run(fmt.Sprintf("%s %s", dockerCompose, strings.Join(dockerComposeExec, " ")), true)
-		return
-	} else if action == "wf" {
-		args := strings.Fields(line)
-		if len(args) < 2 {
-			pterm.Error.WithFatal().Printf("Invalid wf command: %s", line)
-		}
-		WorkflowName := args[1]
-		for _, wf := range workflows {
-			if wf.Name == WorkflowName {
-				pterm.Description.Printfln("Executing workflow: %s", WorkflowName)
-				executeWorkflow(wf, values)
-				break
-			}
-		}
-		return
-	}
-
-	litsOfNotifiesOptions := []string{
-		"notify_success",
-		"notify_error",
-		"notify_warning",
-		"notify_info",
-		"notify",
-	}
-
-	for _, option := range litsOfNotifiesOptions {
-		if action == option {
-			message := strings.TrimPrefix(line, option)
-			message = strings.TrimSpace(message)
-
-			message = strings.TrimPrefix(message, "\"")
-			message = strings.TrimSuffix(message, "\"")
-
-			if option == "notify" {
-				fmt.Println(message)
-			} else if option == "notify_success" {
-				pterm.Success.Println(message)
-			} else if option == "notify_error" {
-				pterm.Error.Println(message)
-			} else if option == "notify_warning" {
-				pterm.Warning.Println(message)
-			} else if option == "notify_info" {
-				pterm.Info.Println(message)
-			}
-
-			return
-			// pterm.DefaultBasicText.Println(message)
-		}
-	}
-
-	pterm.Error.WithFatal().Println("Unknown command or invalid syntax : " + line)
-
-}
-
-func run(line string, printCommand bool) {
-	line = strings.TrimSpace(line)
-	if line == "" {
-		return
-	}
-	if printCommand {
-		pterm.DefaultBasicText.Println(line)
-	}
-	args := strings.Split(line, " ")
-	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Env = os.Environ()
-
-	var stdoutBuf, stderrBuf bytes.Buffer
-
-	cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
-	cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
-	err := cmd.Run()
-	if err != nil {
-		pterm.Error.Println("Error :", err)
-		os.Exit(1)
-	}
-
-}
-
-func getWfFiles(currentDir string) ([]string, error) {
-	return filepath.Glob(filepath.Join(currentDir, "*.wf"))
-}
-
-func getCurrentDir() string {
-	dir, _ := os.Getwd()
-	return dir
-}
-
-func FileGetContents(filename string) string {
-	data, err := os.ReadFile(filename)
-	if err != nil {
-		panic(err)
-	}
-	return string(data)
-}
-
-func Copy(source string, dest string) (bool, error) {
-	fd1, err := os.Open(source)
-	if err != nil {
-		return false, err
-	}
-	defer fd1.Close()
-	fd2, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0644)
-	if err != nil {
-		return false, err
-	}
-	defer fd2.Close()
-	_, e := io.Copy(fd2, fd1)
-	if e != nil {
-		return false, e
-	}
-	return true, nil
-}
-
-func ParseContentToWorkFlowStruct(filename string) map[string]Workflow {
-
-	workflowsMap := map[string]Workflow{}
-	file, err := os.Open(filename)
-	if err != nil {
-		panic(err)
-	}
-
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-	currentWorkflow := Workflow{
-		Name:    "main",
-		Comment: "",
-	}
-	workflowsMap[currentWorkflow.Name] = currentWorkflow
-
-	for scanner.Scan() {
-		line := scanner.Text()
-		line = strings.TrimSpace(line)
-		if line == "" {
-			continue
-		}
-		if strings.HasPrefix(line, "#") {
-			continue
-		}
-
-		workflowName := currentWorkflow.Name
-		comment := currentWorkflow.Comment
-
-		if strings.HasPrefix(line, "[") {
-
-			cursor := 0
-			findWorkflowName := false
-			for i := 0; i < len(line); i++ {
-				if line[i] == ']' {
-					workflowName = strings.TrimPrefix(line[:i], "[")
-					cursor = i
-					findWorkflowName = true
-					break
-				}
-			}
-
-			if !findWorkflowName {
-				continue
-			}
-
-			afterWorkflowNameLine := line[cursor+1:]
-			afterWorkflowNameLine = strings.TrimSpace(afterWorkflowNameLine)
-			line = ""
-			if strings.HasPrefix(afterWorkflowNameLine, "#") {
-				comment = strings.TrimPrefix(afterWorkflowNameLine, "#")
-				comment = strings.TrimSpace(comment)
-			}
-
-		}
-
-		wf, ok := workflowsMap[workflowName]
-		if !ok {
-			wf = Workflow{
-				Name:    workflowName,
-				Comment: comment,
-			}
-			workflowsMap[workflowName] = wf
-		}
-
-		wf.Lines = append(wf.Lines, line)
-		workflowsMap[workflowName] = wf
-		currentWorkflow = wf
-
-		currentWorkflow.Lines = append(currentWorkflow.Lines, line)
-
-	}
-
-	if err := scanner.Err(); err != nil {
-		panic(err)
-	}
-	return workflowsMap
-
-}
-
-func InitDefaultVariables() *map[string]string {
-
-	values := map[string]string{}
-	values["IP_LOCAL"] = GetLocalIP()
-	values["CURRENT_PATH"] = getCurrentDir()
-
-	return &values
-
-}
-
-func GetLocalIP() string {
-	// Récupère toutes les interfaces réseau
-	interfaces, err := net.Interfaces()
-	if err != nil {
-		return ""
-	}
-	for _, i := range interfaces {
-		// Récupère toutes les adresses de chaque interface
-		addrs, err := i.Addrs()
-		if err != nil {
-			return ""
-		}
-
-		for _, addr := range addrs {
-			var ip net.IP
-
-			// Vérifie si l'adresse est de type IP
-			switch v := addr.(type) {
-			case *net.IPNet:
-				ip = v.IP
-			case *net.IPAddr:
-				ip = v.IP
-			}
-
-			// Filtre pour obtenir une adresse IPv4 non-loopback
-			if ip != nil && ip.IsLoopback() == false && ip.To4() != nil {
-				return ip.String()
-			}
-		}
-	}
-	return ""
-}
-
-func TokenGenerator(size int) string {
-	b := make([]byte, size)
-	rand.Read(b)
-	return fmt.Sprintf("%x", b)
-}
-
-func ResolveVariables(values map[string]string, line string) string {
-	for key, value := range values {
-		line = strings.ReplaceAll(line, "${"+key+"}", value)
-	}
-	line = strings.ReplaceAll(line, "${GENERATE_SECRET}", TokenGenerator(32))
-	return line
-
-}
-
-func GetDockerComposeCommand() string {
-	if dockerComposeCommand != "" {
-		return dockerComposeCommand
-	}
-
-	cmd := exec.Command("docker", "compose", "--version")
-	cmd.Env = os.Environ()
-	_, err := cmd.Output()
-	if err != nil {
-		dockerComposeCommand = "docker-compose"
-	} else {
-		dockerComposeCommand = "docker compose"
-	}
-	return dockerComposeCommand
-}
-
-type Workflow struct {
-	Name    string
-	Comment string
-	Lines   []string
-}
+// @Author: F.Michel
+// @github: https://github.com/phpdevcommunity
+package main
+
+import (
+	"bufio"
+	"crypto/rand"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/pterm/pterm"
+	"github.com/urfave/cli/v2"
+)
+
+var (
+	workflows            []Workflow
+	dockerComposeCommand = ""
+	workflowDepth        = 0
+)
+
+func main() {
+	currentDir, _ := os.Getwd()
+	wfFiles, _ := filepath.Glob(filepath.Join(currentDir, "*.wf"))
+
+	if len(wfFiles) == 0 {
+		pterm.Warning.Println("No workflow files found")
+	}
+
+	workflows = []Workflow{}
+	for _, wfFile := range wfFiles {
+		for _, wf := range ParseContentToWorkFlowStruct(wfFile) {
+			if len(wf.Lines) > 0 {
+				workflows = append(workflows, wf)
+			}
+		}
+	}
+
+	Commands := []*cli.Command{}
+	values := InitDefaultVariables()
+	for _, wf := range workflows {
+		Commands = append(Commands, &cli.Command{
+			Name:  wf.Name,
+			Usage: wf.Comment,
+			Action: func(c *cli.Context) error {
+				executeWorkflow(wf, values)
+				return nil
+			},
+		})
+	}
+
+	if len(os.Args) <= 1 {
+		pterm.DefaultBigText.WithLetters(
+			pterm.NewLettersFromStringWithStyle("W", pterm.NewStyle(pterm.FgCyan)),
+			pterm.NewLettersFromStringWithStyle("F", pterm.NewStyle(pterm.FgBlue)),
+		).Render()
+	}
+
+	app := &cli.App{Commands: Commands}
+	if err := app.Run(os.Args); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func executeWorkflow(wf Workflow, values map[string]string) {
+	fmt.Println()
+	start := time.Now()
+
+	depth := workflowDepth
+	workflowDepth++
+	defer func() { workflowDepth-- }()
+
+	indent := ""
+	if depth > 0 {
+		indent = strings.Repeat("  ", depth)
+	}
+
+	if depth == 0 {
+		pterm.DefaultHeader.
+			WithFullWidth().
+			WithBackgroundStyle(pterm.NewStyle(pterm.BgCyan)).
+			WithMargin(1).
+			Printfln("🚀 WORKFLOW: %s", strings.ToUpper(wf.Name))
+	} else {
+		pterm.FgYellow.Printfln("%s╭── 📦 Sub-Workflow: %s", indent, wf.Name)
+	}
+
+	for _, line := range wf.Lines {
+		executeLine(line, values)
+	}
+
+	fmt.Println()
+	elapsed := time.Since(start).Round(time.Millisecond)
+	if depth == 0 {
+		pterm.Success.Printfln("✨ Workflow '%s' completed in %s.", wf.Name, elapsed)
+		fmt.Println()
+	} else {
+		pterm.FgYellow.Printfln("%s╰── ✔ Completed in %s", indent, elapsed)
+	}
+}
+
+func FileExists(filename string) bool {
+	_, err := os.Stat(filename)
+	return err == nil || !os.IsNotExist(err)
+}
+
+func Touch(filename string) {
+	fd, err := os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0666)
+	if err != nil {
+		fatal("Failed to touch file %s: %v", filename, err)
+	}
+	fd.Close()
+}
+
+func Copy(source string, dest string) {
+	fd1, err := os.Open(source)
+	if err != nil {
+		fatal("Failed to open source file %s: %v", source, err)
+	}
+	defer fd1.Close()
+
+	fd2, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0644)
+	if err != nil {
+		fatal("Failed to create destination file %s: %v", dest, err)
+	}
+	defer fd2.Close()
+
+	if _, err := io.Copy(fd2, fd1); err != nil {
+		fatal("Failed to copy %s to %s: %v", source, dest, err)
+	}
+}
+
+func executeLine(lineOriginal string, values map[string]string) {
+	if lineOriginal == "" || strings.HasPrefix(lineOriginal, "#") {
+		return
+	}
+
+	lineOriginal = ResolveVariables(values, lineOriginal)
+	actionOriginal := strings.Split(lineOriginal, " ")[0]
+	action := strings.ToLower(actionOriginal)
+	line := strings.Replace(lineOriginal, actionOriginal, action, 1)
+
+	switch action {
+	case "set":
+		line = strings.TrimPrefix(line, "set ")
+		parts := strings.Split(line, "=")
+		if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
+			fatal("Invalid set command")
+		}
+		values[parts[0]] = parts[1]
+		printLine(pterm.Info.Sprintf("Variable %s set to %s", parts[0], values[parts[0]]))
+
+	case "run":
+		run(strings.TrimPrefix(line, "run "), true)
+
+	case "echo":
+		msg := strings.TrimSpace(strings.TrimPrefix(line, "echo "))
+		if strings.HasPrefix(msg, "\"") && strings.HasSuffix(msg, "\"") {
+			msg = msg[1 : len(msg)-1]
+		}
+		printLine(pterm.DefaultBasicText.Sprint("  "+msg))
+
+	case "exit":
+		os.Exit(0)
+
+	case "touch":
+		fileToCreate := strings.TrimSpace(strings.TrimPrefix(line, "touch "))
+		if fileToCreate == "" {
+			fatal("Invalid touch command")
+		}
+		if FileExists(fileToCreate) {
+			printLine(pterm.Info.Sprintf("%s already exists, skipping...", fileToCreate))
+			return
+		}
+		Touch(fileToCreate)
+		printLine(pterm.Success.Sprintf("%s created", fileToCreate))
+
+	case "copy", "cp":
+		line = strings.TrimPrefix(line, "copy ")
+		parts := strings.Fields(line)
+		if len(parts) < 2 {
+			fatal("Invalid copy command")
+		}
+		fileOrFolder, destination := parts[0], parts[1]
+		if !FileExists(fileOrFolder) {
+			fatal("%s not found", fileOrFolder)
+		}
+		if FileExists(destination) {
+			printLine(pterm.Info.Sprintf("%s already exists, skipping...", destination))
+			return
+		}
+		Copy(fileOrFolder, destination)
+		printLine(pterm.Success.Sprintf("%s copied to %s", fileOrFolder, destination))
+
+	case "mkdir":
+		parts := strings.Fields(line)
+		if len(parts) < 2 {
+			fatal("Invalid mkdir command")
+		}
+		folderName := parts[1]
+		if FileExists(folderName) {
+			printLine(pterm.Info.Sprintf("Folder %s already exists, skipping...", folderName))
+			return
+		}
+		if err := os.MkdirAll(folderName, os.ModePerm); err != nil {
+			fatal(err.Error())
+		}
+		printLine(pterm.Success.Sprintf("Folder '%s' created", folderName))
+
+	case "set_permissions":
+		args := strings.Fields(line)
+		if len(args) < 3 {
+			fatal("Invalid set_permissions command: %s", line)
+		}
+		folderName := args[1]
+		permissions, err := strconv.ParseUint(args[2], 8, 32)
+		if err != nil {
+			fatal("Invalid permissions: %s", args[2])
+		}
+		fi, err := os.Stat(folderName)
+		if err != nil {
+			fatal("Failed to access '%s': %s", folderName, err)
+		}
+		err = filepath.Walk(folderName, func(path string, info os.FileInfo, walkErr error) error {
+			if walkErr != nil {
+				return walkErr
+			}
+			return os.Chmod(path, os.FileMode(permissions))
+		})
+		if err != nil {
+			fatal("Failed to set permissions on '%s': %s", folderName, err)
+		}
+		if fi.IsDir() {
+			printLine(pterm.Info.Sprintf("Permissions 0%o applied recursively to '%s'", permissions, folderName))
+		} else {
+			printLine(pterm.Info.Sprintf("Permissions 0%o applied to '%s'", permissions, folderName))
+		}
+
+	case "sync_time":
+		printLine(pterm.Info.Sprintf("System time: %s", time.Now().Format("2006-01-02 15:04:05 MST")))
+
+	case "docker_compose":
+		args := strings.Fields(line)
+		if len(args) < 2 {
+			fatal("Invalid execute command: %s", line)
+		}
+		run(fmt.Sprintf("%s %s", GetDockerComposeCommand(), strings.Join(args[1:], " ")), true)
+
+	case "wf":
+		args := strings.Fields(line)
+		if len(args) < 2 {
+			fatal("Invalid wf command: %s", line)
+		}
+		wfName := args[1]
+		for _, wf := range workflows {
+			if wf.Name == wfName {
+				executeWorkflow(wf, values)
+				break
+			}
+		}
+
+	case "notify_success", "notify_error", "notify_warning", "notify_info":
+		notifies := map[string]*pterm.PrefixPrinter{
+			"notify_success": &pterm.Success,
+			"notify_error":   &pterm.Error,
+			"notify_warning": &pterm.Warning,
+			"notify_info":    &pterm.Info,
+		}
+		printer := notifies[action]
+		msg := strings.TrimSpace(strings.TrimPrefix(line, action))
+		if strings.HasPrefix(msg, "\"") && strings.HasSuffix(msg, "\"") {
+			msg = msg[1 : len(msg)-1]
+		}
+		printLine(printer.Sprint(msg))
+
+	case "notify":
+		msg := strings.TrimSpace(strings.TrimPrefix(line, "notify"))
+		if strings.HasPrefix(msg, "\"") && strings.HasSuffix(msg, "\"") {
+			msg = msg[1 : len(msg)-1]
+		}
+		printLine(pterm.DefaultBasicText.Sprint("  " + msg))
+
+	default:
+		fatal("Unknown command or invalid syntax : " + line)
+	}
+}
+
+func run(line string, printCommand bool) {
+	line = strings.TrimSpace(line)
+	if line == "" {
+		return
+	}
+
+	var start time.Time
+	indent := ""
+	if workflowDepth > 1 {
+		indent = strings.Repeat("  ", workflowDepth-1)
+	}
+
+	if printCommand {
+		fmt.Println()
+		pterm.FgCyan.Printfln("%s╭─ ▶ %s", indent, line)
+		start = time.Now()
+	}
+
+	args := strings.Fields(line)
+	cmd := exec.Command(args[0], args[1:]...)
+	cmd.Env = os.Environ()
+
+	if printCommand {
+		prefixStr := pterm.FgCyan.Sprint(indent + "│  ")
+		cmd.Stdout = NewPrefixWriter(prefixStr, os.Stdout)
+		cmd.Stderr = NewPrefixWriter(prefixStr, os.Stderr)
+	} else {
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+	}
+
+	err := cmd.Run()
+
+	if printCommand {
+		elapsed := time.Since(start).Round(time.Millisecond)
+		if err != nil {
+			pterm.FgRed.Printfln("%s╰─ ✘ Failed in %s", indent, elapsed)
+		} else {
+			pterm.FgGreen.Printfln("%s╰─ ✔ %s", indent, elapsed)
+		}
+	}
+
+	if err != nil {
+		fmt.Println()
+		pterm.Error.Printfln("Command failed '%s': %v", line, err)
+		os.Exit(1)
+	}
+}
+
+func ParseContentToWorkFlowStruct(filename string) map[string]Workflow {
+	file, err := os.Open(filename)
+	if err != nil {
+		panic(err)
+	}
+	defer file.Close()
+
+	workflowsMap := map[string]Workflow{}
+	scanner := bufio.NewScanner(file)
+	currentWorkflow := Workflow{Name: "main"}
+	workflowsMap[currentWorkflow.Name] = currentWorkflow
+
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line == "" || strings.HasPrefix(line, "#") {
+			continue
+		}
+
+		if strings.HasPrefix(line, "[") {
+			end := strings.Index(line, "]")
+			if end == -1 {
+				continue
+			}
+			wfName := line[1:end]
+			comment := ""
+			after := strings.TrimSpace(line[end+1:])
+			if strings.HasPrefix(after, "#") {
+				comment = strings.TrimSpace(strings.TrimPrefix(after, "#"))
+			}
+
+			currentWorkflow = Workflow{Name: wfName, Comment: comment}
+			workflowsMap[wfName] = currentWorkflow
+			continue
+		}
+
+		currentWorkflow.Lines = append(currentWorkflow.Lines, line)
+		workflowsMap[currentWorkflow.Name] = currentWorkflow
+	}
+	return workflowsMap
+}
+
+func InitDefaultVariables() map[string]string {
+	return map[string]string{
+		"IP_LOCAL":     GetLocalIP(),
+		"CURRENT_PATH": getCurrentDir(),
+	}
+}
+
+func getCurrentDir() string {
+	dir, _ := os.Getwd()
+	return dir
+}
+
+func GetLocalIP() string {
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		return ""
+	}
+	for _, addr := range addrs {
+		if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
+			return ipNet.IP.String()
+		}
+	}
+	return ""
+}
+
+func TokenGenerator(size int) string {
+	b := make([]byte, size)
+	rand.Read(b)
+	return fmt.Sprintf("%x", b)
+}
+
+func ResolveVariables(values map[string]string, line string) string {
+	for key, value := range values {
+		line = strings.ReplaceAll(line, "${"+key+"}", value)
+	}
+	return strings.ReplaceAll(line, "${GENERATE_SECRET}", TokenGenerator(32))
+}
+
+func GetDockerComposeCommand() string {
+	if dockerComposeCommand == "" {
+		if err := exec.Command("docker", "compose", "--version").Run(); err != nil {
+			dockerComposeCommand = "docker-compose"
+		} else {
+			dockerComposeCommand = "docker compose"
+		}
+	}
+	return dockerComposeCommand
+}
+
+type Workflow struct {
+	Name    string
+	Comment string
+	Lines   []string
+}
+
+type PrefixWriter struct {
+	Prefix []byte
+	Writer io.Writer
+	isBOL  bool
+}
+
+func NewPrefixWriter(prefix string, writer io.Writer) *PrefixWriter {
+	return &PrefixWriter{
+		Prefix: []byte(prefix),
+		Writer: writer,
+		isBOL:  true,
+	}
+}
+
+func (pw *PrefixWriter) Write(p []byte) (n int, err error) {
+	for _, b := range p {
+		if pw.isBOL {
+			if _, err = pw.Writer.Write(pw.Prefix); err != nil {
+				return n, err
+			}
+			pw.isBOL = false
+		}
+		if _, err = pw.Writer.Write([]byte{b}); err != nil {
+			return n, err
+		}
+		n++
+		if b == '\n' {
+			pw.isBOL = true
+		}
+	}
+	return n, nil
+}
+
+func printLine(text string) {
+	if workflowDepth > 1 {
+		fmt.Print(strings.Repeat("  ", workflowDepth-1))
+	}
+	fmt.Println(text)
+}
+
+func fatal(format string, a ...interface{}) {
+	printLine(pterm.Error.Sprintf(format, a...))
+	os.Exit(1)
+}