fix: copy-on-wrap commands and typed errors

Clone commands before wrapping Before hooks, export ErrLifecycleNotInContext
and ErrNoDependencies, add Run helper, split command.go and deps.go.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 18:04:07 -07:00
parent 60ecc69620
commit f64f7fa04e
9 changed files with 282 additions and 100 deletions
+104
View File
@@ -0,0 +1,104 @@
package appcli
import (
"context"
"gitea.auvem.com/go-toolkit/app"
"github.com/urfave/cli/v3"
)
// NewCommand wraps cmd with dependency loading in Before. The input command is
// not mutated; a shallow copy is returned.
func NewCommand(cmdcfg *cli.Command, depfn ...DepFn) *cli.Command {
if len(depfn) == 0 {
return cmdcfg
}
cmd := cloneCommand(cmdcfg)
originalBefore := cmdcfg.Before
cmd.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, ErrLifecycleNotInContext
}
deps := make([]*app.Module, 0)
for _, fn := range depfn {
if fn == nil {
continue
}
modules, err := fn(lifecycle, cmd)
if err != nil {
return ctx, err
}
deps = append(deps, modules...)
}
if len(deps) > 0 {
if err := lifecycle.Require(deps...); err != nil {
return ctx, err
}
}
return ctx, nil
}
return cmd
}
// NewRootCommand wraps a root command, adds a global --verbose / -v flag, and
// applies dependency loading via [NewCommand].
func NewRootCommand(cmdcfg *cli.Command, depfn ...DepFn) *cli.Command {
root := cloneCommand(cmdcfg)
if !hasFlag(root, "verbose") {
root.Flags = append(root.Flags, &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Enable verbose output",
})
}
return NewCommand(root, depfn...)
}
// VerboseFromCommand reports whether --verbose was set on the command or an ancestor.
func VerboseFromCommand(cmd *cli.Command) bool {
if cmd == nil {
return false
}
return cmd.Bool("verbose")
}
func cloneCommand(src *cli.Command) *cli.Command {
if src == nil {
return nil
}
dup := *src
if len(src.Flags) > 0 {
dup.Flags = append([]cli.Flag(nil), src.Flags...)
}
if len(src.Commands) > 0 {
dup.Commands = append([]*cli.Command(nil), src.Commands...)
}
if len(src.Arguments) > 0 {
dup.Arguments = append([]cli.Argument(nil), src.Arguments...)
}
return &dup
}
func hasFlag(cmd *cli.Command, name string) bool {
for _, f := range cmd.Flags {
if bf, ok := f.(*cli.BoolFlag); ok && bf.Name == name {
return true
}
}
return false
}