package cli import ( "fmt" "os" "path/filepath" "strings" "github.com/adrg/xdg" "github.com/spf13/cobra" ) // RootFlags holds global flags that apply to all commands. // These flags are defined on the root command and inherited by all subcommands. type RootFlags struct { ConfigPath string Verbose bool Debug bool Quiet bool SkipErrors bool } var rootFlags RootFlags // NewRootCommand creates the root cobra command for the vaultik CLI. // It sets up the command structure, global flags, and adds all subcommands. // This is the main entry point for the CLI command hierarchy. func NewRootCommand() *cobra.Command { cmd := &cobra.Command{ Use: "vaultik", Short: "Secure incremental backup tool with asymmetric encryption", Long: `vaultik is a secure incremental backup tool that encrypts data using age public keys and uploads to S3-compatible storage. No private keys are needed on the source system.`, SilenceUsage: true, // Bare 'vaultik' (no subcommand): print help. The banner is // printed once at process startup by CLIEntry, before cobra // parses arguments, so it appears even when cobra rejects // args (e.g. "requires at least 2 arg(s)") and on --help. Run: func(cmd *cobra.Command, args []string) { _ = cmd.Help() }, } // Add global flags cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "", "Path to config file (default: $VAULTIK_CONFIG or platform config dir)") cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false, "Enable verbose output") cmd.PersistentFlags().BoolVar(&rootFlags.Debug, "debug", false, "Enable debug output") cmd.PersistentFlags().BoolVarP(&rootFlags.Quiet, "quiet", "q", false, "Suppress non-error output") cmd.PersistentFlags().BoolVar(&rootFlags.SkipErrors, "skip-errors", false, "Continue past per-file errors instead of aborting (applies to snapshot create and restore)") // Add subcommands cmd.AddCommand( NewConfigCommand(), NewPruneCommand(), NewStoreCommand(), NewSnapshotCommand(), NewInfoCommand(), NewVersionCommand(), NewRemoteCommand(), NewDatabaseCommand(), ) return cmd } // GetRootFlags returns the global flags that were parsed from the command line. // This allows subcommands to access global flag values like verbosity and config path. func GetRootFlags() RootFlags { return rootFlags } // ResolveConfigPath resolves the config file path from flags, environment, or default. // Search order: --config flag, VAULTIK_CONFIG env, XDG config dir, /etc/vaultik/config.yml. // Explicit paths from --config and $VAULTIK_CONFIG are checked for existence // so the user gets a clear error instead of a downstream YAML parser failure. func ResolveConfigPath() (string, error) { if path := rootFlags.ConfigPath; path != "" { if _, err := os.Stat(path); err != nil { return "", fmt.Errorf("config file from --config not found: %s (run 'vaultik config init --config %s' to create it)", path, path) } return path, nil } if path := os.Getenv("VAULTIK_CONFIG"); path != "" { if _, err := os.Stat(path); err != nil { return "", fmt.Errorf("config file from $VAULTIK_CONFIG not found: %s (unset VAULTIK_CONFIG, point it at an existing file, or run 'vaultik config init')", path) } return path, nil } for _, path := range defaultConfigPaths() { if _, err := os.Stat(path); err == nil { return path, nil } } return "", fmt.Errorf("no config file found at %s (run 'vaultik config init' to create the default config, or pass --config )", strings.Join(defaultConfigPaths(), " or ")) } // defaultConfigPaths returns the ordered list of config paths to search. // On macOS: ~/Library/Application Support/vaultik/config.yml // On Linux: ~/.config/vaultik/config.yml // Fallback: /etc/vaultik/config.yml func defaultConfigPaths() []string { return []string{ filepath.Join(xdg.ConfigHome, "vaultik", "config.yml"), "/etc/vaultik/config.yml", } } // DefaultConfigPath returns the platform-appropriate default config path. // Used by the init command and in help text. func DefaultConfigPath() string { if os.Getuid() == 0 { return "/etc/vaultik/config.yml" } return filepath.Join(xdg.ConfigHome, "vaultik", "config.yml") }