package appcli import ( "context" "errors" "gitea.auvem.com/go-toolkit/app" "github.com/urfave/cli/v3" ) // DepFn is a function type that returns a slice of dependencies. type DepFn func(*app.Lifecycle, *cli.Command) ([]*app.Module, error) // DepList is a convenience function that returns a DepFn that uses a // predefined list of dependecies. func DepList(deps ...*app.Module) DepFn { return func(_ *app.Lifecycle, _ *cli.Command) ([]*app.Module, error) { if len(deps) == 0 { return nil, errors.New("no dependencies provided") } return deps, nil } } // 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, depfn DepFn) *cli.Command { if depfn == nil { 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") } deps, err := depfn(lifecycle, cmd) if err != nil { return ctx, err } if err := lifecycle.Require(deps...); 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 // DepFn is a function that returns the modules that this command depends // on. Note: All root dependencies will be inherited by all subcommands. DepFn DepFn } // 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.DepFn) } // 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") }