Files
Dominik Schulz 7c63ba09b7 chore(deps): migrate from urfave/cli v2 to v3 (#3428)
Migrate the entire codebase from github.com/urfave/cli/v2 to
github.com/urfave/cli/v3 (v3.9.0).

Key breaking changes addressed:
- cli.App removed, replaced by *cli.Command
- ActionFunc signature: func(*Context) error -> func(context.Context, *Command) error
- BeforeFunc signature: func(*Context) error -> func(context.Context, *Command) (context.Context, error)
- app.RunContext -> app.Run
- app.EnableBashCompletion -> app.EnableShellCompletion
- Subcommands field renamed to Commands
- EnvVars on flags -> Sources: cli.EnvVars(...)
- cli.NewContext removed; test helpers updated to use cmd.Run() pattern
- cli.Flag interface updated (Apply removed, Get/PreParse/PostParse/Set added)
- VersionPrinter type changed to func(*Command)

Also updates .capabilities.json baseline to reflect new cli/v3 call paths.

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
2026-05-16 18:13:19 +02:00

244 lines
7.4 KiB
Go

package action
import (
"context"
"fmt"
"github.com/gopasspw/gopass/internal/action/exit"
"github.com/gopasspw/gopass/internal/backend"
"github.com/gopasspw/gopass/internal/config"
"github.com/gopasspw/gopass/internal/cui"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/fsutil"
"github.com/gopasspw/gopass/pkg/termio"
"github.com/urfave/cli/v3"
)
const logo = `
__ _ _ _ _ _ ___ ___
/'_ '\ /'_'\ ( '_'\ /'_' )/',__)/',__)
( (_) |( (_) )| (_) )( (_| |\__, \\__, \
'\__ |'\___/'| ,__/''\__,_)(____/(____/
( )_) | | |
\___/' (_)
`
// IsInitialized returns an error if the store is not properly
// prepared.
func (s *setupHandler) IsInitialized(ctx context.Context, cmd *cli.Command) (context.Context, error) {
ctx = ctxutil.WithGlobalFlags(ctx, cmd)
inited, err := s.Store.IsInitialized(ctx)
if err != nil {
return ctx, exit.Error(exit.Unknown, err, "Failed to initialize store: %s", err)
}
if inited {
debug.Log("Store is fully initialized and ready to go\n\nAll systems go. 🚀\n")
var name string
if cmd.Args() != nil {
name = cmd.Args().First()
}
// setting the mount point here is not enough when we're using the REPL mode
ctx = config.WithMount(ctx, s.Store.MountPoint(name))
s.printReminderFn(ctx)
if cmd.Name != "sync" && !cmd.Bool("nosync") {
_ = s.autoSyncFn(ctx)
}
return ctx, nil
}
debug.Log("Store needs to be initialized.\n\nAbort. Abort. Abort. 🚫\n")
if !ctxutil.IsInteractive(ctx) {
return ctx, exit.Error(exit.NotInitialized, nil, "password-store is not initialized. Try '%s init'", s.Name)
}
out.Printf(ctx, logo)
out.Printf(ctx, "🌟 Welcome to gopass!")
out.Noticef(ctx, "No existing configuration found.")
contSetup, err := termio.AskForBool(ctx, "❓ Do you want to continue to setup?", false)
if err != nil {
return ctx, err
}
if contSetup {
return ctx, s.Setup(ctx, cmd)
}
out.Printf(ctx, "☝ Please run 'gopass setup'")
return ctx, exit.Error(exit.NotInitialized, err, "not initialized")
}
// Init a new password store with a first gpg id.
func (s *setupHandler) Init(ctx context.Context, cmd *cli.Command) error {
ctx = ctxutil.WithGlobalFlags(ctx, cmd)
path := cmd.String("path")
alias := cmd.String("store")
ctx, err := initParseContext(ctx, cmd)
if err != nil {
return err
}
out.Printf(ctx, "🍭 Initializing a new password store ...")
if name := termio.DetectName(ctx, cmd); name != "" {
ctx = ctxutil.WithUsername(ctx, name)
}
if email := termio.DetectEmail(ctx, cmd); email != "" {
ctx = ctxutil.WithEmail(ctx, email)
}
inited, err := s.Store.IsInitialized(ctx)
if err != nil {
return exit.Error(exit.Unknown, err, "Failed to initialized store: %s", err)
}
if inited {
out.Errorf(ctx, "Store is already initialized!")
}
if err := s.init(ctx, alias, path, cmd.Args().Slice()...); err != nil {
return exit.Error(exit.Unknown, err, "Failed to initialize store: %s", err)
}
return nil
}
func initParseContext(ctx context.Context, cmd *cli.Command) (context.Context, error) {
if cmd.IsSet("crypto") {
var err error
ctx, err = backend.WithCryptoBackendString(ctx, cmd.String("crypto"))
if err != nil {
return ctx, exit.Error(exit.Unknown, err, "Failed to set crypto backend: %s", err)
}
}
if cmd.IsSet("storage") {
var err error
ctx, err = backend.WithStorageBackendString(ctx, cmd.String("storage"))
if err != nil {
return ctx, exit.Error(exit.Unknown, err, "Failed to set storage backend: %s", err)
}
}
if !backend.HasCryptoBackend(ctx) {
debug.Log("Using default Crypto Backend (GPGCLI)")
ctx = backend.WithCryptoBackend(ctx, backend.GPGCLI)
}
if !backend.HasStorageBackend(ctx) {
debug.Log("Using default storage backend (GitFS)")
ctx = backend.WithStorageBackend(ctx, backend.GitFS)
}
sb := backend.GetStorageBackend(ctx)
if sb == backend.CryptFS {
out.Warning(ctx, "⚠ CryptFS is an experimental backend. Use at your own risk! ⚠")
}
if sb == backend.JJFS {
out.Warning(ctx, "⚠ JJFS is an experimental backend. Use at your own risk! ⚠")
}
return ctx, nil
}
func (s *setupHandler) init(ctx context.Context, alias, path string, keys ...string) error {
if path == "" {
if alias != "" {
path = config.PwStoreDir(alias)
} else {
path = s.Store.Path()
}
}
path = fsutil.CleanPath(path)
debug.Log("Initializing Store %q in %q for %+v", alias, path, keys)
out.Printf(ctx, "🔑 Searching for usable private Keys ...")
debug.Log("Checking private keys for: %+v", keys)
crypto := s.getCryptoFor(ctx, alias)
// private key selection doesn't matter for plain. save one question.
// TODO should ask the backend
if crypto.Name() == "plain" {
keys, _ = crypto.ListIdentities(ctx)
}
if len(keys) < 1 {
if crypto.Name() != "age" {
out.Notice(ctx, "Hint: Use 'gopass init <subkey> to use subkeys!'")
}
nk, err := cui.AskForPrivateKey(ctx, crypto, "🎮 Please select a private key for encrypting secrets:")
if err != nil {
out.Noticef(ctx, "Hint: Use 'gopass setup --crypto %s' to be guided through an initial setup instead of 'gopass init'", crypto.Name())
return fmt.Errorf("failed to read user input: %w", err)
}
keys = []string{nk}
}
debug.Log("Initializing sub store - Alias: %q - Path: %q - Keys: %+v", alias, path, keys)
if err := s.Store.Init(ctx, alias, path, keys...); err != nil {
return fmt.Errorf("failed to init store %q at %q: %w", alias, path, err)
}
// Persist the selected storage backend to the config so it is used on
// subsequent runs and accidental backend switches (e.g. jjfs taking over
// when a .jj directory appears) are prevented.
if backend.HasStorageBackend(ctx) {
cfgMount := alias
if cfgMount == "" {
cfgMount = "<root>"
}
bn := backend.StorageBackendName(backend.GetStorageBackend(ctx))
cfg, _ := config.FromContext(ctx)
if err := cfg.Set(cfgMount, "storage.backend", bn); err != nil {
debug.Log("failed to persist storage backend %q for mount %q: %s", bn, cfgMount, err)
} else {
debug.Log("Persisted storage backend %q to config for mount %q", bn, cfgMount)
}
}
if alias != "" && path != "" {
debug.Log("Mounting sub store %q -> %q", alias, path)
if err := s.Store.AddMount(ctx, alias, path); err != nil {
return fmt.Errorf("failed to add mount %q: %w", alias, err)
}
}
if backend.HasStorageBackend(ctx) {
bn := backend.StorageBackendName(backend.GetStorageBackend(ctx))
debug.Log("Initializing RCS (%s) ...", bn)
if err := s.rcsInit(ctx, alias, ctxutil.GetUsername(ctx), ctxutil.GetEmail(ctx)); err != nil {
debug.Log("Stacktrace: %+v\n", err)
out.Errorf(ctx, "❌ Failed to init Version Control (%s): %s", bn, err)
}
debug.Log("RCS initialized as %s", s.Store.Storage(ctx, alias).Name())
} else {
debug.Log("not initializing RCS backend ...")
}
out.Printf(ctx, "🏁 Password store %s initialized for:", path)
s.printRecipients(ctx, alias)
return nil
}
func (s *setupHandler) printRecipients(ctx context.Context, alias string) {
crypto := s.Store.Crypto(ctx, alias)
for _, recipient := range s.Store.ListRecipients(ctx, alias) {
if kl, err := crypto.FindRecipients(ctx, recipient); err == nil && len(kl) > 0 {
recipient = crypto.FormatKey(ctx, kl[0], "")
}
out.Printf(ctx, "📩 "+recipient)
}
}
func (s *setupHandler) getCryptoFor(ctx context.Context, name string) backend.Crypto {
return s.Store.Crypto(ctx, name)
}