Repository: goravel/framework
Pull request #994: feat: [#653] Update github.com/urfave/cli/v2 to v3
Author: login: almas-x
Labels: name: 🚀 Review Ready
Created: 2025-04-07T07:42:45Z | Merged: 2025-04-07T14:43:28Z

## 📑 Description

Closes https://github.com/goravel/goravel/issues/653

<!-- Please add Review Ready tag or leave a Review Ready message when the PR is good to go -->
<!-- More description can be written after this -->

## ✅ Checks

<!-- Make sure your PR passes the CI checks and do check the following fields as needed - -->
- [x] Added test cases for my code



Diff:
diff --git a/console/application.go b/console/application.go
index 4ae49ca9b..10e483648 100644
--- a/console/application.go
+++ b/console/application.go
@@ -1,11 +1,12 @@
 package console
 
 import (
+	"context"
 	"os"
 	"slices"
 	"strings"
 
-	"github.com/urfave/cli/v2"
+	"github.com/urfave/cli/v3"
 
 	"github.com/goravel/framework/contracts/console"
 	"github.com/goravel/framework/contracts/console/command"
@@ -16,32 +17,25 @@ import (
 var (
 	noANSI     bool
 	noANSIFlag = &cli.BoolFlag{
-		Name:               "no-ansi",
-		Destination:        &noANSI,
-		DisableDefaultText: true,
-		Usage:              "Force disable ANSI output",
-	}
-
-	globalFlags = []cli.Flag{
-		noANSIFlag,
-		cli.HelpFlag,
-		cli.VersionFlag,
+		Name:        "no-ansi",
+		Destination: &noANSI,
+		HideDefault: true,
+		Usage:       "Force disable ANSI output",
 	}
 )
 
 type Application struct {
-	instance   *cli.App
+	instance   *cli.Command
 	useArtisan bool
 }
 
 // NewApplication Create a new Artisan application.
 // Will add artisan flag to the command if useArtisan is true.
 func NewApplication(name, usage, usageText, version string, useArtisan bool) console.Artisan {
-	instance := cli.NewApp()
+	instance := &cli.Command{}
 	instance.Name = name
 	instance.Usage = usage
 	instance.UsageText = usageText
-	instance.HelpName = name + " [global options]"
 	instance.Version = version
 	instance.CommandNotFound = commandNotFound
 	instance.OnUsageError = onUsageError
@@ -59,8 +53,8 @@ func (r *Application) Register(commands []console.Command) {
 		cliCommand := cli.Command{
 			Name:  item.Signature(),
 			Usage: item.Description(),
-			Action: func(ctx *cli.Context) error {
-				return item.Handle(NewCliContext(ctx))
+			Action: func(_ context.Context, cmd *cli.Command) error {
+				return item.Handle(NewCliContext(cmd))
 			},
 			Category:     item.Extend().Category,
 			ArgsUsage:    item.Extend().ArgsUsage,
@@ -105,6 +99,8 @@ func (r *Application) CallAndExit(command string) {
 func (r *Application) Run(args []string, exitIfArtisan bool) error {
 	if noANSI || env.IsNoANSI() || slices.Contains(args, "--no-ansi") {
 		color.Disable()
+	} else {
+		color.Enable()
 	}
 
 	artisanIndex := -1
@@ -126,7 +122,7 @@ func (r *Application) Run(args []string, exitIfArtisan bool) error {
 		}
 
 		cliArgs := append([]string{args[0]}, args[artisanIndex+1:]...)
-		if err := r.instance.Run(cliArgs); err != nil {
+		if err := r.instance.Run(context.Background(), cliArgs); err != nil {
 			if exitIfArtisan {
 				panic(err.Error())
 			}
@@ -149,16 +145,16 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 		case command.FlagTypeBool:
 			flag := flag.(*command.BoolFlag)
 			cliFlags = append(cliFlags, &cli.BoolFlag{
-				Name:               flag.Name,
-				Aliases:            flag.Aliases,
-				DisableDefaultText: flag.DisableDefaultText,
-				Usage:              flag.Usage,
-				Required:           flag.Required,
-				Value:              flag.Value,
+				Name:        flag.Name,
+				Aliases:     flag.Aliases,
+				HideDefault: flag.DisableDefaultText,
+				Usage:       flag.Usage,
+				Required:    flag.Required,
+				Value:       flag.Value,
 			})
 		case command.FlagTypeFloat64:
 			flag := flag.(*command.Float64Flag)
-			cliFlags = append(cliFlags, &cli.Float64Flag{
+			cliFlags = append(cliFlags, &cli.FloatFlag{
 				Name:     flag.Name,
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
@@ -167,12 +163,12 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 			})
 		case command.FlagTypeFloat64Slice:
 			flag := flag.(*command.Float64SliceFlag)
-			cliFlags = append(cliFlags, &cli.Float64SliceFlag{
+			cliFlags = append(cliFlags, &cli.FloatSliceFlag{
 				Name:     flag.Name,
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    cli.NewFloat64Slice(flag.Value...),
+				Value:    cli.NewFloatSlice(flag.Value...).Value(),
 			})
 		case command.FlagTypeInt:
 			flag := flag.(*command.IntFlag)
@@ -181,20 +177,25 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    flag.Value,
+				Value:    int64(flag.Value),
 			})
 		case command.FlagTypeIntSlice:
 			flag := flag.(*command.IntSliceFlag)
+			var int64Slice []int64
+			for _, v := range flag.Value {
+				int64Slice = append(int64Slice, int64(v))
+			}
+
 			cliFlags = append(cliFlags, &cli.IntSliceFlag{
 				Name:     flag.Name,
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    cli.NewIntSlice(flag.Value...),
+				Value:    cli.NewIntSlice(int64Slice...).Value(),
 			})
 		case command.FlagTypeInt64:
 			flag := flag.(*command.Int64Flag)
-			cliFlags = append(cliFlags, &cli.Int64Flag{
+			cliFlags = append(cliFlags, &cli.IntFlag{
 				Name:     flag.Name,
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
@@ -203,12 +204,12 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 			})
 		case command.FlagTypeInt64Slice:
 			flag := flag.(*command.Int64SliceFlag)
-			cliFlags = append(cliFlags, &cli.Int64SliceFlag{
+			cliFlags = append(cliFlags, &cli.IntSliceFlag{
 				Name:     flag.Name,
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    cli.NewInt64Slice(flag.Value...),
+				Value:    cli.NewIntSlice(flag.Value...).Value(),
 			})
 		case command.FlagTypeString:
 			flag := flag.(*command.StringFlag)
@@ -226,7 +227,7 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    cli.NewStringSlice(flag.Value...),
+				Value:    cli.NewStringSlice(flag.Value...).Value(),
 			})
 		}
 	}
diff --git a/console/application_test.go b/console/application_test.go
index 2588944e1..1432593a3 100644
--- a/console/application_test.go
+++ b/console/application_test.go
@@ -4,7 +4,7 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/urfave/cli/v2"
+	"github.com/urfave/cli/v3"
 
 	"github.com/goravel/framework/contracts/console"
 	"github.com/goravel/framework/contracts/console/command"
@@ -58,7 +58,7 @@ func TestFlagsToCliFlags(t *testing.T) {
 			assert.Equal(t, boolFlag.Value, cliBoolFlag.Value)
 		case command.FlagTypeFloat64:
 			float64Flag := flag.(*command.Float64Flag)
-			cliFloat64Flag := cliFlags[i].(*cli.Float64Flag)
+			cliFloat64Flag := cliFlags[i].(*cli.FloatFlag)
 			assert.Equal(t, float64Flag.Name, cliFloat64Flag.Name)
 			assert.Equal(t, float64Flag.Aliases, cliFloat64Flag.Aliases)
 			assert.Equal(t, float64Flag.Usage, cliFloat64Flag.Usage)
@@ -66,12 +66,12 @@ func TestFlagsToCliFlags(t *testing.T) {
 			assert.Equal(t, float64Flag.Value, cliFloat64Flag.Value)
 		case command.FlagTypeFloat64Slice:
 			float64SliceFlag := flag.(*command.Float64SliceFlag)
-			cliFloat64SliceFlag := cliFlags[i].(*cli.Float64SliceFlag)
+			cliFloat64SliceFlag := cliFlags[i].(*cli.FloatSliceFlag)
 			assert.Equal(t, float64SliceFlag.Name, cliFloat64SliceFlag.Name)
 			assert.Equal(t, float64SliceFlag.Aliases, cliFloat64SliceFlag.Aliases)
 			assert.Equal(t, float64SliceFlag.Usage, cliFloat64SliceFlag.Usage)
 			assert.Equal(t, float64SliceFlag.Required, cliFloat64SliceFlag.Required)
-			assert.Equal(t, cli.NewFloat64Slice(float64SliceFlag.Value...), cliFloat64SliceFlag.Value)
+			assert.Equal(t, cli.NewFloatSlice(float64SliceFlag.Value...).Value(), cliFloat64SliceFlag.Value)
 		case command.FlagTypeInt:
 			intFlag := flag.(*command.IntFlag)
 			cliIntFlag := cliFlags[i].(*cli.IntFlag)
@@ -79,7 +79,7 @@ func TestFlagsToCliFlags(t *testing.T) {
 			assert.Equal(t, intFlag.Aliases, cliIntFlag.Aliases)
 			assert.Equal(t, intFlag.Usage, cliIntFlag.Usage)
 			assert.Equal(t, intFlag.Required, cliIntFlag.Required)
-			assert.Equal(t, intFlag.Value, cliIntFlag.Value)
+			assert.Equal(t, intFlag.Value, int(cliIntFlag.Value))
 		case command.FlagTypeIntSlice:
 			intSliceFlag := flag.(*command.IntSliceFlag)
 			cliIntSliceFlag := cliFlags[i].(*cli.IntSliceFlag)
@@ -87,10 +87,15 @@ func TestFlagsToCliFlags(t *testing.T) {
 			assert.Equal(t, intSliceFlag.Aliases, cliIntSliceFlag.Aliases)
 			assert.Equal(t, intSliceFlag.Usage, cliIntSliceFlag.Usage)
 			assert.Equal(t, intSliceFlag.Required, cliIntSliceFlag.Required)
-			assert.Equal(t, cli.NewIntSlice(intSliceFlag.Value...), cliIntSliceFlag.Value)
+
+			var intSlice []int64
+			for _, v := range intSliceFlag.Value {
+				intSlice = append(intSlice, int64(v))
+			}
+			assert.Equal(t, cli.NewIntSlice(intSlice...).Value(), cliIntSliceFlag.Value)
 		case command.FlagTypeInt64:
 			int64Flag := flag.(*command.Int64Flag)
-			cliInt64Flag := cliFlags[i].(*cli.Int64Flag)
+			cliInt64Flag := cliFlags[i].(*cli.IntFlag)
 			assert.Equal(t, int64Flag.Name, cliInt64Flag.Name)
 			assert.Equal(t, int64Flag.Aliases, cliInt64Flag.Aliases)
 			assert.Equal(t, int64Flag.Usage, cliInt64Flag.Usage)
@@ -98,12 +103,12 @@ func TestFlagsToCliFlags(t *testing.T) {
 			assert.Equal(t, int64Flag.Value, cliInt64Flag.Value)
 		case command.FlagTypeInt64Slice:
 			int64SliceFlag := flag.(*command.Int64SliceFlag)
-			cliInt64SliceFlag := cliFlags[i].(*cli.Int64SliceFlag)
+			cliInt64SliceFlag := cliFlags[i].(*cli.IntSliceFlag)
 			assert.Equal(t, int64SliceFlag.Name, cliInt64SliceFlag.Name)
 			assert.Equal(t, int64SliceFlag.Aliases, cliInt64SliceFlag.Aliases)
 			assert.Equal(t, int64SliceFlag.Usage, cliInt64SliceFlag.Usage)
 			assert.Equal(t, int64SliceFlag.Required, cliInt64SliceFlag.Required)
-			assert.Equal(t, cli.NewInt64Slice(int64SliceFlag.Value...), cliInt64SliceFlag.Value)
+			assert.Equal(t, cli.NewIntSlice(int64SliceFlag.Value...).Value(), cliInt64SliceFlag.Value)
 		case command.FlagTypeString:
 			stringFlag := flag.(*command.StringFlag)
 			cliStringFlag := cliFlags[i].(*cli.StringFlag)
@@ -119,7 +124,7 @@ func TestFlagsToCliFlags(t *testing.T) {
 			assert.Equal(t, stringSliceFlag.Aliases, cliStringSliceFlag.Aliases)
 			assert.Equal(t, stringSliceFlag.Usage, cliStringSliceFlag.Usage)
 			assert.Equal(t, stringSliceFlag.Required, cliStringSliceFlag.Required)
-			assert.Equal(t, cli.NewStringSlice(stringSliceFlag.Value...), cliStringSliceFlag.Value)
+			assert.Equal(t, cli.NewStringSlice(stringSliceFlag.Value...).Value(), cliStringSliceFlag.Value)
 		}
 	}
 }
diff --git a/console/cli_context.go b/console/cli_context.go
index 71b19ce41..6c5a24144 100644
--- a/console/cli_context.go
+++ b/console/cli_context.go
@@ -8,17 +8,17 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/mattn/go-runewidth"
 	"github.com/pterm/pterm"
-	"github.com/urfave/cli/v2"
+	"github.com/urfave/cli/v3"
 
 	"github.com/goravel/framework/contracts/console"
 	"github.com/goravel/framework/support/color"
 )
 
 type CliContext struct {
-	instance *cli.Context
+	instance *cli.Command
 }
 
-func NewCliContext(instance *cli.Context) *CliContext {
+func NewCliContext(instance *cli.Command) *CliContext {
 	return &CliContext{instance}
 }
 
@@ -124,7 +124,7 @@ func (r *CliContext) Confirm(question string, option ...console.ConfirmOption) b
 
 	if err := input.Value(&answer).Run(); err != nil {
 		r.Error(err.Error())
-		
+
 		return false
 	}
 
@@ -194,27 +194,32 @@ func (r *CliContext) OptionBool(key string) bool {
 }
 
 func (r *CliContext) OptionFloat64(key string) float64 {
-	return r.instance.Float64(key)
+	return r.instance.Float(key)
 }
 
 func (r *CliContext) OptionFloat64Slice(key string) []float64 {
-	return r.instance.Float64Slice(key)
+	return r.instance.FloatSlice(key)
 }
 
 func (r *CliContext) OptionInt(key string) int {
-	return r.instance.Int(key)
+	return int(r.instance.Int(key))
 }
 
 func (r *CliContext) OptionIntSlice(key string) []int {
-	return r.instance.IntSlice(key)
+	var intSlice []int
+	for _, v := range r.instance.IntSlice(key) {
+		intSlice = append(intSlice, int(v))
+	}
+
+	return intSlice
 }
 
 func (r *CliContext) OptionInt64(key string) int64 {
-	return r.instance.Int64(key)
+	return r.instance.Int(key)
 }
 
 func (r *CliContext) OptionInt64Slice(key string) []int64 {
-	return r.instance.Int64Slice(key)
+	return r.instance.IntSlice(key)
 }
 
 func (r *CliContext) Secret(question string, option ...console.SecretOption) (string, error) {
diff --git a/console/cli_helper.go b/console/cli_helper.go
index 7c9e2ea66..eb2fb7e3b 100644
--- a/console/cli_helper.go
+++ b/console/cli_helper.go
@@ -1,17 +1,18 @@
 package console
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"os"
-	"slices"
 	"sort"
 	"strings"
 	"text/tabwriter"
 	"text/template"
+	"unicode"
 
 	"github.com/charmbracelet/huh"
-	"github.com/urfave/cli/v2"
+	"github.com/urfave/cli/v3"
 	"github.com/xrash/smetrics"
 
 	"github.com/goravel/framework/support/color"
@@ -20,8 +21,9 @@ import (
 
 func init() {
 	cli.HelpPrinterCustom = printHelpCustom
-	cli.AppHelpTemplate = appHelpTemplate
+	cli.RootCommandHelpTemplate = appHelpTemplate
 	cli.CommandHelpTemplate = commandHelpTemplate
+	cli.SubcommandHelpTemplate = commandHelpTemplate
 	cli.VersionPrinter = printVersion
 	huh.ErrUserAborted = cli.Exit(color.Red().Sprint("Cancelled."), 0)
 }
@@ -35,7 +37,7 @@ var (
 {{ yellow "Usage:" }}
    {{if .UsageText}}{{wrap (colorize .UsageText) 3}}{{end}}{{if .VisibleFlags}}
 
-{{ yellow "Global options:" }}{{template "flagTemplate" .VisibleFlags}}{{end}}{{if .VisibleCommands}}
+{{ yellow "Global options:" }}{{template "flagTemplate" (sortVisibleFlags .)}}{{end}}{{if .VisibleCommands}}
 
 {{ yellow "Available commands:" }}{{template "commandTemplate" .}}{{end}}
 `
@@ -44,18 +46,18 @@ var (
    {{ (colorize .Usage) }}
 
 {{ yellow "Usage:" }}
-   {{template "usageTemplate" .}}{{if globalFlags}}
+   {{template "usageTemplate" .}}{{with $root := .Root}}
 
-{{ yellow "Global options:" }}{{template "flagTemplate" globalFlags}}{{end}}{{if .VisibleFlags}}
+{{ yellow "Global options:" }}{{template "flagTemplate"  (sortVisibleFlags $root)}}{{end}}{{if .VisibleFlags}}
 
-{{ yellow "Options:" }}{{template "flagTemplate" .VisibleFlags}}{{end}}
+{{ yellow "Options:" }}{{template "flagTemplate" (sortVisibleFlags .)}}{{end}}
 `
 	commandTemplate = `{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCategories}}{{if .Name}}
  {{yellow .Name}}:{{end}}{{range (sortCommands .VisibleCommands)}}
   {{$s := join .Names ", "}}{{green $s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap (colorize .Usage) $cv}}{{end}}{{end}}`
-	flagTemplate = `{{ $cv := offsetFlags . 5}}{{range  (sortFlags .)}}
+	flagTemplate = `{{ $cv := offsetFlags . 5}}{{range  .}}
    {{$s := getFlagName .}}{{green $s}}{{ $sp := subtract $cv (offset $s 1) }}{{ indent $sp ""}}{{$us := (capitalize .Usage)}}{{wrap (colorize $us) $cv}}{{$df := getFlagDefaultText . }}{{if $df}} {{yellow $df}}{{end}}{{end}}`
-	usageTemplate = `{{if .UsageText}}{{wrap (colorize .UsageText) 3}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [options]{{end}}{{if .ArgsUsage}}{{.ArgsUsage}}{{else}}{{if .Args}} [arguments...]{{end}}{{end}}{{end}}`
+	usageTemplate = `{{if .UsageText}}{{wrap (colorize .UsageText) 3}}{{else}}{{(helpName .FullName)}}{{if .VisibleFlags}} [options]{{end}}{{if .ArgsUsage}}{{.ArgsUsage}}{{else}}{{if .Args}} [arguments...]{{end}}{{end}}{{end}}`
 )
 
 // colorsFuncMap is a map of functions for coloring text.
@@ -88,14 +90,14 @@ func colorize(text string) string {
 
 }
 
-func commandNotFound(ctx *cli.Context, command string) {
+func commandNotFound(_ context.Context, cmd *cli.Command, command string) {
 	var (
 		msgTxt     = fmt.Sprintf("Command '%s' is not defined.", command)
 		suggestion string
 	)
 	if alternatives := findAlternatives(command, func() (collection []string) {
-		for i := range ctx.App.Commands {
-			collection = append(collection, ctx.App.Commands[i].Names()...)
+		for i := range cmd.Commands {
+			collection = append(collection, cmd.Commands[i].Names()...)
 		}
 		return
 	}()); len(alternatives) > 0 {
@@ -174,7 +176,7 @@ func findAlternatives(name string, collection []string) (result []string) {
 
 func getFlagDefaultText(flag cli.DocGenerationFlag) string {
 	defaultValueString := ""
-	if bf, ok := flag.(*cli.BoolFlag); !ok || !bf.DisableDefaultText {
+	if bf, ok := flag.(*cli.BoolFlag); !ok || bf.IsDefaultVisible() {
 		if s := flag.GetDefaultText(); s != "" {
 			defaultValueString = fmt.Sprintf(`[default: %s]`, s)
 		}
@@ -183,7 +185,7 @@ func getFlagDefaultText(flag cli.DocGenerationFlag) string {
 }
 
 func getFlagName(flag cli.Flag) string {
-	names := flag.(cli.DocGenerationFlag).Names()
+	names := flag.Names()
 	sort.Slice(names, func(i, j int) bool {
 		return len(names[i]) < len(names[j])
 	})
@@ -196,11 +198,61 @@ func getFlagName(flag cli.Flag) string {
 	return prefixed
 }
 
+func handleNoANSI() {
+	if noANSI || env.IsNoANSI() {
+		color.Disable()
+	} else {
+		color.Enable()
+	}
+}
+
+func helpName(fullName string) string {
+	var namePath []string
+	for i, name := range strings.Split(fullName, " ") {
+		namePath = append(namePath, name)
+		if i == 0 {
+			namePath = append(namePath, "[global options]")
+		}
+	}
+
+	if len(namePath) > 1 {
+		fullName = strings.Join(namePath, " ")
+	}
+
+	return fullName
+}
+
 func indent(spaces int, v string) string {
 	pad := strings.Repeat(" ", spaces)
 	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
 }
 
+// lexicographicLess compares strings alphabetically considering case.
+func lexicographicLess(i, j string) bool {
+	iRunes := []rune(i)
+	jRunes := []rune(j)
+
+	lenShared := len(iRunes)
+	if lenShared > len(jRunes) {
+		lenShared = len(jRunes)
+	}
+
+	for index := 0; index < lenShared; index++ {
+		ir := iRunes[index]
+		jr := jRunes[index]
+
+		if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr {
+			return lir < ljr
+		}
+
+		if ir != jr {
+			return ir < jr
+		}
+	}
+
+	return i < j
+}
+
 func offset(input string, fixed int) int {
 	return len(input) + fixed
 }
@@ -225,12 +277,12 @@ func offsetFlags(flags []cli.Flag, fixed int) int {
 	return maxLen + fixed
 }
 
-func onUsageError(_ *cli.Context, err error, _ bool) error {
+func onUsageError(_ context.Context, _ *cli.Command, err error, _ bool) error {
 	if flag, ok := strings.CutPrefix(err.Error(), "flag provided but not defined: -"); ok {
 		color.Red().Printfln("The '%s' option does not exist.", flag)
 		return nil
 	}
-	if flag, ok := strings.CutPrefix(err.Error(), "flag needs an argument: -"); ok {
+	if flag, ok := strings.CutPrefix(err.Error(), "flag needs an argument: "); ok {
 		color.Red().Printfln("The '%s' option requires a value.", flag)
 		return nil
 	}
@@ -249,15 +301,15 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, _ map[string
 	funcMap := template.FuncMap{
 		"capitalize":         capitalize,
 		"colorize":           colorize,
-		"globalFlags":        func() []cli.Flag { return globalFlags },
 		"getFlagName":        getFlagName,
 		"getFlagDefaultText": getFlagDefaultText,
 		"indent":             indent,
+		"helpName":           helpName,
 		"join":               strings.Join,
 		"offset":             offset,
 		"offsetCommands":     offsetCommands,
 		"offsetFlags":        offsetFlags,
-		"sortFlags":          sortFlags,
+		"sortVisibleFlags":   sortVisibleFlags,
 		"sortCommands":       sortCommands,
 		"subtract":           subtract,
 		"trim":               strings.TrimSpace,
@@ -277,9 +329,8 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, _ map[string
 		}
 	}
 
-	if noANSI || env.IsNoANSI() {
-		color.Disable()
-	}
+	handleNoANSI()
+
 	err := t.Execute(w, data)
 	if err != nil {
 		// If the writer is closed, t.Execute will fail, and there's nothing
@@ -296,20 +347,35 @@ func printTemplateError(err error) {
 	}
 }
 
-func printVersion(ctx *cli.Context) {
-	_, _ = fmt.Fprintf(ctx.App.Writer, "%v %v\n", ctx.App.Usage, color.Green().Sprint(ctx.App.Version))
+func printVersion(cmd *cli.Command) {
+	handleNoANSI()
+
+	_, _ = fmt.Fprintf(cmd.Writer, "%v %v\n", cmd.Usage, color.Green().Sprint(cmd.Version))
 }
 
 func sortCommands(commands []*cli.Command) []*cli.Command {
-	sort.Sort(cli.CommandsByName(commands))
+	sort.Slice(commands, func(i, j int) bool {
+		return lexicographicLess(commands[i].Name, commands[j].Name)
+	})
 
 	return commands
 }
 
-func sortFlags(flags []cli.Flag) []cli.Flag {
+func sortVisibleFlags(cmd *cli.Command) []cli.Flag {
+	var (
+		flags       = cmd.VisibleFlags()
+		globalFlags = cmd.Root().VisibleFlags()
+	)
 	sort.Sort(cli.FlagsByName(flags))
+
+	globalFlagNames := make(map[string]struct{})
+	for i := range globalFlags {
+		globalFlagNames[getFlagName(globalFlags[i])] = struct{}{}
+	}
 	sort.Slice(flags, func(i, j int) bool {
-		return slices.Contains(globalFlags, flags[j]) && !slices.Contains(globalFlags, flags[i])
+		_, isGlobalI := globalFlagNames[getFlagName(flags[i])]
+		_, isGlobalJ := globalFlagNames[getFlagName(flags[j])]
+		return !isGlobalI && isGlobalJ
 	})
 
 	return flags
diff --git a/console/cli_helper_test.go b/console/cli_helper_test.go
index 2524ce3ad..d9230bd4c 100644
--- a/console/cli_helper_test.go
+++ b/console/cli_helper_test.go
@@ -24,7 +24,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) {
 		containsOutput []string
 	}{
 		{
-			name: "print app help",
+			name: "print_app_help",
 			containsOutput: []string{
 				color.Yellow().Sprint("Usage:"),
 				color.Yellow().Sprint("Global options:"),
@@ -35,33 +35,59 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) {
 			},
 		},
 		{
-			name: "print command help",
+			name: "print_command_help",
 			call: "help test:foo",
 			containsOutput: []string{
 				color.Yellow().Sprint("Description:"),
 				color.Yellow().Sprint("Usage:"),
+				color.Yellow().Sprint("Global options:"),
+				color.Green().Sprint("-h, --help"),
+				color.Green().Sprint("    --no-ansi"),
+				color.Green().Sprint("-v, --version"),
 				color.Yellow().Sprint("Options:"),
 				color.Green().Sprint("-b, --bool"),
 				color.Green().Sprint("-i, --int"),
 				color.Blue().Sprint("int"),
+				color.Green().Sprint("-h, --help"),
 			},
 		},
 		{
-			name: "print version",
+			name: "print_command_help(check_flag_sorted)",
+			call: "help --no-ansi test:foo",
+			containsOutput: []string{
+				`Description:
+   Test command
+
+Usage:
+   test [global options] test:foo [options]
+
+Global options:
+   -h, --help       Show help
+       --no-ansi    Force disable ANSI output
+   -v, --version    Print the version
+
+Options:
+   -b, --bool    Bool flag [default: false]
+   -i, --int     int flag [default: 0]
+   -h, --help    Show help`,
+			},
+		},
+		{
+			name: "print_version",
 			call: "--version",
 			containsOutput: []string{
 				"test " + color.Green().Sprint("test"),
 			},
 		},
 		{
-			name: "command not found",
+			name: "command_not_found",
 			call: "not-found",
 			containsOutput: []string{
 				color.New(color.FgLightRed).Sprint("Command 'not-found' is not defined."),
 			},
 		},
 		{
-			name: "command not found(suggest)",
+			name: "command_not_found(suggest)",
 			call: "test",
 			containsOutput: []string{
 				color.New(color.FgLightRed).Sprint("Command 'test' is not defined. Did you mean one of these?"),
@@ -70,7 +96,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) {
 			},
 		},
 		{
-			name: "command not found(suggest)",
+			name: "command_not_found(suggest)",
 			call: "fo",
 			containsOutput: []string{
 				color.New(color.FgLightRed).Sprint("Command 'fo' is not defined. Did you mean this?"),
@@ -78,28 +104,28 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) {
 			},
 		},
 		{
-			name: "option not found",
+			name: "option_not_found",
 			call: "test:foo --not-found",
 			containsOutput: []string{
 				color.Red().Sprint("The 'not-found' option does not exist."),
 			},
 		},
 		{
-			name: "option needs a value",
+			name: "option_needs_a_value",
 			call: "test:foo --int",
 			containsOutput: []string{
-				color.Red().Sprint("The 'int' option requires a value."),
+				color.Red().Sprint("The '--int' option requires a value."),
 			},
 		},
 		{
-			name: "option value is not valid",
+			name: "option_value_is_not_valid",
 			call: "test:foo --int not-a-number",
 			containsOutput: []string{
 				color.Red().Sprint("Invalid value 'not-a-number' for option 'int'."),
 			},
 		},
 		{
-			name: "no ansi color",
+			name: "no_ansi_color",
 			call: "--no-ansi",
 			containsOutput: []string{
 				"test test",
@@ -171,3 +197,28 @@ func (receiver *TestFooCommand) Handle(_ console.Context) error {
 func (receiver *TestBarCommand) Signature() string {
 	return "test:bar"
 }
+
+func TestLexicographicLess(t *testing.T) {
+	tests := []struct {
+		i        string
+		j        string
+		expected bool
+	}{
+		{"", "a", true},
+		{"a", "", false},
+		{"a", "a", false},
+		{"a", "A", false},
+		{"A", "a", true},
+		{"aa", "a", false},
+		{"a", "aa", true},
+		{"a", "b", true},
+		{"a", "B", true},
+		{"A", "b", true},
+		{"A", "B", true},
+	}
+
+	for _, tt := range tests {
+		actual := lexicographicLess(tt.i, tt.j)
+		assert.Equal(t, tt.expected, actual)
+	}
+}
diff --git a/go.mod b/go.mod
index 1eb3aac3c..552478947 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
 	github.com/spf13/cast v1.7.1
 	github.com/spf13/viper v1.20.1
 	github.com/stretchr/testify v1.10.0
-	github.com/urfave/cli/v2 v2.27.6
+	github.com/urfave/cli/v3 v3.1.1
 	github.com/vektra/mockery/v2 v2.53.2
 	golang.org/x/crypto v0.37.0
 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
@@ -69,7 +69,6 @@ require (
 	github.com/charmbracelet/x/exp/strings v0.0.0-20241222104055-e1130b311607 // indirect
 	github.com/charmbracelet/x/term v0.2.1 // indirect
 	github.com/containerd/console v1.0.4 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
@@ -96,7 +95,6 @@ require (
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/rogpeppe/go-internal v1.13.1 // indirect
-	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sagikazarmark/locafero v0.8.0 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/spf13/afero v1.14.0 // indirect
diff --git a/go.sum b/go.sum
index 749d378cd..1e7a799d8 100644
--- a/go.sum
+++ b/go.sum
@@ -66,7 +66,6 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX
 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
 github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
@@ -217,7 +216,6 @@ github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
 github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
-github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
 github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
@@ -250,8 +248,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
-github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
+github.com/urfave/cli/v3 v3.1.1 h1:bNnl8pFI5dxPOjeONvFCDFoECLQsceDG4ejahs4Jtxk=
+github.com/urfave/cli/v3 v3.1.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/vektra/mockery/v2 v2.53.2 h1:4G/4fl9x722Yb8hLqH1YU3XZNRJFwl5KUMvpkmAyuC8=
 github.com/vektra/mockery/v2 v2.53.2/go.mod h1:UJT+mgXhCcOCHXTnM5cJHCZL+d76BYB+EbY1sFztEB8=
 github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
diff --git a/support/color/color.go b/support/color/color.go
index 87a8638af..341b92ec3 100644
--- a/support/color/color.go
+++ b/support/color/color.go
@@ -200,3 +200,8 @@ func CaptureOutput(f func(w io.Writer)) string {
 func Disable() {
 	pterm.DisableColor()
 }
+
+// Enable enables color output
+func Enable() {
+	pterm.EnableColor()
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 6 out of 7 changed files in this pull request and generated no comments.

<details>
<summary>Files not reviewed (1)</summary>

* **go.mod**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**console/cli_context.go:218**
* Verify that r.instance.Int(key) returns an int64 value as expected in urfave/cli v3; if it returns an int, consider applying an explicit conversion to int64 for consistency in OptionInt64.
```
return r.instance.Int(key)
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T07:43:19Z
user: login: hwbrzzl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T07:55:23Z
user: login: almas-x
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T07:59:33Z
user: login: hwbrzzl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T08:02:14Z
user: login: almas-x
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T08:03:25Z
user: login: devhaozi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T08:29:47Z
user: login: hwbrzzl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T08:36:07Z
user: login: almas-x
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T09:15:53Z
user: login: hwbrzzl
body: Thanks, LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:30:37Z

Review comments:
diff_hunk: @@ -196,11 +199,53 @@ func getFlagName(flag cli.Flag) string {
 	return prefixed
 }
 
+func helpName(fullName string) string {
+	var namePath []string
+	for i, name := range strings.Split(fullName, " ") {
+		namePath = append(namePath, name)
+		if i == 0 {
+			namePath = append(namePath, "[global options]")
+		}
+	}
+
+	if len(namePath) > 1 {
+		fullName = strings.Join(namePath, " ")
+	}
+
+	return fullName
+}
+
 func indent(spaces int, v string) string {
 	pad := strings.Repeat(" ", spaces)
 	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
 }
 
+// lexicographicLess compares strings alphabetically considering case.
+func lexicographicLess(i, j string) bool {
path: console/cli_helper.go
user: login: hwbrzzl
body: Could you add some test cases for this new function?
created_at: 2025-04-07T07:51:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,20 +181,25 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    flag.Value,
+				Value:    int64(flag.Value),
 			})
 		case command.FlagTypeIntSlice:
 			flag := flag.(*command.IntSliceFlag)
+			var int64Slice []int64
+			for _, v := range flag.Value {
+				int64Slice = append(int64Slice, int64(v))
+			}
path: console/application.go
user: login: hwbrzzl
body: The cli code is weird: `IntSliceFlag` vs `[]int64`

<img width="979" alt="image" src="https://github.com/user-attachments/assets/e203fc0c-42be-404f-b4f0-a19851b008e5" />

created_at: 2025-04-07T07:55:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,11 +199,53 @@ func getFlagName(flag cli.Flag) string {
 	return prefixed
 }
 
+func helpName(fullName string) string {
+	var namePath []string
+	for i, name := range strings.Split(fullName, " ") {
+		namePath = append(namePath, name)
+		if i == 0 {
+			namePath = append(namePath, "[global options]")
+		}
+	}
+
+	if len(namePath) > 1 {
+		fullName = strings.Join(namePath, " ")
+	}
+
+	return fullName
+}
+
 func indent(spaces int, v string) string {
 	pad := strings.Repeat(" ", spaces)
 	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
 }
 
+// lexicographicLess compares strings alphabetically considering case.
+func lexicographicLess(i, j string) bool {
path: console/cli_helper.go
user: login: almas-x
body: This function is copied from [here](https://github.com/urfave/cli/blob/20335c61280dd50c922910e323451076bc90002b/sort.go#L6), since the cli.CommandsByName function was removed in v3. I simply reused the original implementation from the source. Given that it’s unchanged, do we still need to add additional test cases for it?
created_at: 2025-04-07T07:59:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,11 +199,53 @@ func getFlagName(flag cli.Flag) string {
 	return prefixed
 }
 
+func helpName(fullName string) string {
+	var namePath []string
+	for i, name := range strings.Split(fullName, " ") {
+		namePath = append(namePath, name)
+		if i == 0 {
+			namePath = append(namePath, "[global options]")
+		}
+	}
+
+	if len(namePath) > 1 {
+		fullName = strings.Join(namePath, " ")
+	}
+
+	return fullName
+}
+
 func indent(spaces int, v string) string {
 	pad := strings.Repeat(" ", spaces)
 	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
 }
 
+// lexicographicLess compares strings alphabetically considering case.
+func lexicographicLess(i, j string) bool {
path: console/cli_helper.go
user: login: hwbrzzl
body: Yes, we can add some test cases to show the action of this function. It should be very easy to add it via AI.
created_at: 2025-04-07T08:02:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,20 +181,25 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    flag.Value,
+				Value:    int64(flag.Value),
 			})
 		case command.FlagTypeIntSlice:
 			flag := flag.(*command.IntSliceFlag)
+			var int64Slice []int64
+			for _, v := range flag.Value {
+				int64Slice = append(int64Slice, int64(v))
+			}
path: console/application.go
user: login: almas-x
body: Yeah, it’s definitely weird. In v3, the original `int` was replaced with `int64`, so the flag no longer returns an `int`. I added a conversion in `console/application.go `to preserve the original behavior.
created_at: 2025-04-07T08:03:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,20 +181,25 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    flag.Value,
+				Value:    int64(flag.Value),
 			})
 		case command.FlagTypeIntSlice:
 			flag := flag.(*command.IntSliceFlag)
+			var int64Slice []int64
+			for _, v := range flag.Value {
+				int64Slice = append(int64Slice, int64(v))
+			}
path: console/application.go
user: login: devhaozi
body: I think `Int*` can removed, only keep `Int64*`.
created_at: 2025-04-07T08:29:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,20 +181,25 @@ func flagsToCliFlags(flags []command.Flag) []cli.Flag {
 				Aliases:  flag.Aliases,
 				Usage:    flag.Usage,
 				Required: flag.Required,
-				Value:    flag.Value,
+				Value:    int64(flag.Value),
 			})
 		case command.FlagTypeIntSlice:
 			flag := flag.(*command.IntSliceFlag)
+			var int64Slice []int64
+			for _, v := range flag.Value {
+				int64Slice = append(int64Slice, int64(v))
+			}
path: console/application.go
user: login: hwbrzzl
body: Yes, `case command.FlagTypeInt` and `case command.FlagTypeInt64` can use the same flow (in our framework, still distinguish FlagTypeInt and FlagTypeInt64). The same with `slice`.
created_at: 2025-04-07T08:36:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,11 +199,53 @@ func getFlagName(flag cli.Flag) string {
 	return prefixed
 }
 
+func helpName(fullName string) string {
+	var namePath []string
+	for i, name := range strings.Split(fullName, " ") {
+		namePath = append(namePath, name)
+		if i == 0 {
+			namePath = append(namePath, "[global options]")
+		}
+	}
+
+	if len(namePath) > 1 {
+		fullName = strings.Join(namePath, " ")
+	}
+
+	return fullName
+}
+
 func indent(spaces int, v string) string {
 	pad := strings.Repeat(" ", spaces)
 	return pad + strings.Replace(v, "\n", "\n"+pad, -1)
 }
 
+// lexicographicLess compares strings alphabetically considering case.
+func lexicographicLess(i, j string) bool {
path: console/cli_helper.go
user: login: almas-x
body: Done!
created_at: 2025-04-07T09:15:53Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-07T07:45:58Z
body: ## [Codecov](https://app.codecov.io/gh/goravel/framework/pull/994?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel) Report
Attention: Patch coverage is `87.03704%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 70.18%. Comparing base [(`9e85faf`)](https://app.codecov.io/gh/goravel/framework/commit/9e85faf606837545960ac1adf7abb97996af84cb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel) to head [(`b98c632`)](https://app.codecov.io/gh/goravel/framework/commit/b98c632563e13dae255ccd59d46cc985c66a1de1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/goravel/framework/pull/994?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel) | Patch % | Lines |
|---|---|---|
| [console/cli\_context.go](https://app.codecov.io/gh/goravel/framework/pull/994?src=pr&el=tree&filepath=console%2Fcli_context.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel#diff-Y29uc29sZS9jbGlfY29udGV4dC5nbw==) | 8.33% | [11 Missing :warning: ](https://app.codecov.io/gh/goravel/framework/pull/994?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel) |
| [support/color/color.go](https://app.codecov.io/gh/goravel/framework/pull/994?src=pr&el=tree&filepath=support%2Fcolor%2Fcolor.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel#diff-c3VwcG9ydC9jb2xvci9jb2xvci5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/goravel/framework/pull/994?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel) |
| [console/application.go](https://app.codecov.io/gh/goravel/framework/pull/994?src=pr&el=tree&filepath=console%2Fapplication.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel#diff-Y29uc29sZS9hcHBsaWNhdGlvbi5nbw==) | 96.00% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/goravel/framework/pull/994?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #994      +/-   ##
==========================================
+ Coverage   70.04%   70.18%   +0.14%     
==========================================
  Files         168      168              
  Lines       11353    11428      +75     
==========================================
+ Hits         7952     8021      +69     
- Misses       3050     3056       +6     
  Partials      351      351              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/goravel/framework/pull/994?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goravel).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: almas-x
created_at: 2025-04-07T08:44:20Z
body: Improved the flag sorting logic based on the feedback in [this comment](https://github.com/goravel/framework/pull/884#issuecomment-2653130129), ensuring consistent output order.
author_association: CONTRIBUTOR
user: login: almas-x
created_at: 2025-04-07T09:37:02Z
body: I’ve added a new function:
```go
func handleNoANSI() {
	if noANSI || env.IsNoANSI() {
		color.Disable()
	} else {
		color.Enable()
	}
}
```

This ensures that the --no-ansi option doesn’t have side effects between commands.
For example, in the following case:

```go
type Test struct {
}

// Signature The name and signature of the console command.
func (r *Test) Signature() string {
	return "app:test"
}

// Description The console command description.
func (r *Test) Description() string {
	return "Command description"
}

// Extend The console command extend.
func (r *Test) Extend() command.Extend {
	return command.Extend{Category: "app"}
}

// Handle Execute the console command.
func (r *Test) Handle(ctx console.Context) error {
	artisan := facades.Artisan()
	_ = artisan.Call("help --no-ansi build")
	ctx.NewLine()
	_ = artisan.Call("help migrate")
	return nil
}
```

Result:

<img width="521" alt="image" src="https://github.com/user-attachments/assets/7c5e03e5-3e6e-4cb6-bd68-51bbcf05bc7d" />

<img width="574" alt="image" src="https://github.com/user-attachments/assets/3d095c9c-6242-4d52-abb8-2ad2456fb79e" />

author_association: CONTRIBUTOR