package cli import ( "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/adrg/xdg" "github.com/spf13/cobra" "sneak.berlin/go/vaultik/internal/globals" "sneak.berlin/go/vaultik/internal/ui" ) // bannerOnce ensures the banner is printed at most once per process, // even if multiple cobra hooks (PersistentPreRun, Help, Run) would // otherwise each call maybePrintBanner. var bannerOnce sync.Once // maybePrintBanner prints the application banner unless an output- // suppression flag is active. Safe to call multiple times — it prints // at most once per process. func maybePrintBanner(cmd *cobra.Command) { if rootFlags.Quiet { return } if cronFlag := cmd.Flags().Lookup("cron"); cronFlag != nil && cronFlag.Value.String() == "true" { return } bannerOnce.Do(func() { short := globals.Commit if len(short) > 12 { short = short[:12] } writeStartupBanner(ui.New(os.Stdout), time.Now().UTC(), short) }) } // 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, // Banner before every subcommand invocation that doesn't // suppress output. fx setupGlobals will not print it again. PersistentPreRun: func(cmd *cobra.Command, args []string) { maybePrintBanner(cmd) }, // Bare 'vaultik' (no subcommand): banner + help. Run: func(cmd *cobra.Command, args []string) { maybePrintBanner(cmd) _ = cmd.Help() }, } // Help output (--help and group-level cmds) also gets the banner. defaultHelp := cmd.HelpFunc() cmd.SetHelpFunc(func(c *cobra.Command, args []string) { maybePrintBanner(c) defaultHelp(c, args) }) // 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(), NewRestoreCommand(), 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") }