From 6b4196dbda59b80db8e0945bfb41db0989abbd00 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Wed, 4 Jun 2025 15:17:59 -0700 Subject: [PATCH] initial commit --- README.md | 3 ++ appcli.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 ++++ go.sum | 12 ++++++ 4 files changed, 147 insertions(+) create mode 100644 README.md create mode 100644 appcli.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/README.md b/README.md new file mode 100644 index 0000000..9126e5b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# appcli + +appcli provides boilerplate dependency handling for command line apps based on [urfave/cli](https://github.com/urfave/cli) that utilize [app](https://gitea.auvem.com/go-toolkit/app). diff --git a/appcli.go b/appcli.go new file mode 100644 index 0000000..e39325e --- /dev/null +++ b/appcli.go @@ -0,0 +1,124 @@ +package appcli + +import ( + "context" + "errors" + + "gitea.auvem.com/go-toolkit/app" + "github.com/urfave/cli/v3" +) + +// NewCommand creates a new CLI command with the specified configuration and +// and dependencies. Returns a standard *cli.Command that can be used directly +// with urfave/cli/v3 types. If any dependencies are provided, the Before method +// is overriden to ensure that all dependencies are satisfied before the command +// is executed. Requires an app.Lifecycle to be present in the context when the +// command is executed. +func NewCommand(cmdcfg *cli.Command, depends ...*app.Module) *cli.Command { + if len(depends) == 0 { + return cmdcfg + } + + // Override the Before method to handle dependencies + originalBefore := cmdcfg.Before + cmdcfg.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) { + if originalBefore != nil { + var err error + ctx, err = originalBefore(ctx, cmd) + if err != nil { + return ctx, err + } + } + + lifecycle := app.LifecycleFromContext(ctx) + if lifecycle == nil { + return ctx, errors.New("lifecycle not found in context, cannot run command with dependencies") + } + + if err := lifecycle.Require(depends...); err != nil { + return ctx, err + } + return ctx, nil + } + + return cmdcfg +} + +// RootCommandOpts defines the options for creating a root command. +type RootCommandOpts struct { + // Command is the base command configuration. + Command *cli.Command + + // Dependencies are the modules that this command depends on. Note: All + // root dependencies will be inherited by all subcommands. + Dependencies []*app.Module +} + +// NewRootCommand creates a new root CLI command with the specified configuration. +// Override Before and After to take over app.Lifecycle setup and teardown. Adds +// a verbose flag. See NewCommand for more details on how dependencies are handled. +// Requires an app.Lifecycle to be present in the context when the command is executed. +func NewRootCommand(rootcfg *RootCommandOpts) *cli.Command { + cmdcfg := rootcfg.Command + + cmdcfg.Flags = append(cmdcfg.Flags, &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"v"}, + Usage: "Enable verbose output", + }) + + // Override the before method to handle logger and lifecycle + originalBefore := cmdcfg.Before + cmdcfg.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) { + if originalBefore != nil { + var err error + ctx, err = originalBefore(ctx, cmd) + if err != nil { + return ctx, err + } + } + + lifecycle := app.LifecycleFromContext(ctx) + if lifecycle == nil { + return ctx, errors.New("lifecycle not found in context, run root command") + } + + if err := lifecycle.Setup(); err != nil { + return ctx, err + } + + return ctx, nil + } + + // Override the After method to handle cleanup if needed + originalAfter := cmdcfg.After + cmdcfg.After = func(ctx context.Context, cmd *cli.Command) error { + if originalAfter != nil { + if err := originalAfter(ctx, cmd); err != nil { + return err + } + } + + lifecycle := app.LifecycleFromContext(ctx) + if lifecycle == nil { + return errors.New("lifecycle not found in context, cannot clean up root command") + } + + if err := lifecycle.Teardown(); err != nil { + return err + } + + return nil + } + + return NewCommand(cmdcfg, rootcfg.Dependencies...) +} + +// VerboseFromCommand checks if the verbose flag is set in the command context. +func VerboseFromCommand(cmd *cli.Command) bool { + if cmd == nil { + return false + } + + return cmd.Bool("verbose") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4cf3a6d --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module gitea.auvem.com/go-toolkit/appcli + +go 1.24.0 + +require ( + gitea.auvem.com/go-toolkit/app v0.0.0-20250603235859-6f9e3731acf9 + github.com/urfave/cli/v3 v3.3.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..838a6cd --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +gitea.auvem.com/go-toolkit/app v0.0.0-20250603235859-6f9e3731acf9 h1:MYOI+bB4IBAqoL1tyIUFnu0S+NSq0OX88J3K/PUR7lI= +gitea.auvem.com/go-toolkit/app v0.0.0-20250603235859-6f9e3731acf9/go.mod h1:a7ENpOxndUdONE6oZ9MZAvG1ba2uq01x/LtcnDkpOj8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= +github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=