package cmd import ( "context" "fmt" "net" "net/url" "os" "os/signal" "strings" "time" "github.com/getsentry/sentry-go" "github.com/go-errors/errors" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "golang.org/x/mod/semver" ) const ( groupQuickStart = "quick-start" groupLocalDev = "local-dev" groupManagementAPI = "management-api" ) func IsManagementAPI(cmd *cobra.Command) bool { for cmd != cmd.Root() { if cmd.GroupID == groupManagementAPI { return true } // Find the last assigned group if len(cmd.GroupID) > 0 { break } cmd = cmd.Parent() } return false } func promptLogin(fsys afero.Fs) error { if _, err := utils.LoadAccessTokenFS(fsys); err == utils.ErrMissingToken { utils.CmdSuggestion = fmt.Sprintf("Run %s first.", utils.Aqua("supabase login")) return errors.New("You need to be logged-in in order to use Management API commands.") } else { return err } } var experimental = []*cobra.Command{ bansCmd, restrictionsCmd, vanityCmd, sslEnforcementCmd, genKeysCmd, postgresCmd, branchesCmd, storageCmd, } func IsExperimental(cmd *cobra.Command) bool { for _, exp := range experimental { if cmd == exp || cmd.Parent() == exp { return true } } return false } var ( sentryOpts = sentry.ClientOptions{ Dsn: utils.SentryDsn, Release: utils.Version, ServerName: "", // Set TracesSampleRate to 1.0 to capture 100% // of transactions for performance monitoring. // We recommend adjusting this value in production, TracesSampleRate: 1.0, } createTicket bool rootCmd = &cobra.Command{ Use: "supabase", Short: "Supabase CLI " + utils.Version, Version: utils.Version, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if IsExperimental(cmd) && !viper.GetBool("EXPERIMENTAL") { return errors.New("must set the --experimental flag to run this command") } cmd.SilenceUsage = true // Change workdir fsys := afero.NewOsFs() if err := utils.ChangeWorkDir(fsys); err != nil { return err } // Add common flags ctx := cmd.Context() if IsManagementAPI(cmd) { if err := promptLogin(fsys); err != nil { return err } ctx, _ = signal.NotifyContext(ctx, os.Interrupt) if cmd.Flags().Lookup("project-ref") != nil { if err := flags.ParseProjectRef(ctx, fsys); err != nil { return err } } } if err := flags.ParseDatabaseConfig(cmd.Flags(), fsys); err != nil { return err } // Prepare context if viper.GetBool("DEBUG") { ctx = utils.WithTraceContext(ctx) fmt.Fprintln(os.Stderr, cmd.Root().Short) } cmd.SetContext(ctx) // Setup sentry last to ignore errors from parsing cli flags apiHost, err := url.Parse(utils.GetSupabaseAPIHost()) if err != nil { return err } sentryOpts.Environment = apiHost.Host return sentry.Init(sentryOpts) }, SilenceErrors: true, } ) func Execute() { defer recoverAndExit() if err := rootCmd.Execute(); err != nil { panic(err) } // Check upgrade last because --version flag is initialised after execute version, err := checkUpgrade(rootCmd.Context(), afero.NewOsFs()) if err != nil { fmt.Fprintln(utils.GetDebugLogger(), err) } if semver.Compare(version, "v"+utils.Version) > 0 { fmt.Fprintln(os.Stderr, suggestUpgrade(version)) } if len(utils.CmdSuggestion) > 0 { fmt.Fprintln(os.Stderr, utils.CmdSuggestion) } } func checkUpgrade(ctx context.Context, fsys afero.Fs) (string, error) { if shouldFetchRelease(fsys) { version, err := utils.GetLatestRelease(ctx) if exists, _ := afero.DirExists(fsys, utils.SupabaseDirPath); exists { // If user is offline, write an empty file to skip subsequent checks err = utils.WriteFile(utils.CliVersionPath, []byte(version), fsys) } return version, err } version, err := afero.ReadFile(fsys, utils.CliVersionPath) if err != nil { return "", errors.Errorf("failed to read cli version: %w", err) } return string(version), nil } func shouldFetchRelease(fsys afero.Fs) bool { // Always fetch latest release when using --version flag if vf := rootCmd.Flag("version"); vf != nil && vf.Changed { return true } if fi, err := fsys.Stat(utils.CliVersionPath); err == nil { expiry := fi.ModTime().Add(time.Hour * 10) // Skip if last checked is less than 10 hours ago return time.Now().After(expiry) } return true } func suggestUpgrade(version string) string { const guide = "https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli" return fmt.Sprintf(`A new version of Supabase CLI is available: %s (currently installed v%s) We recommend updating regularly for new features and bug fixes: %s`, utils.Yellow(version), utils.Version, utils.Bold(guide)) } func recoverAndExit() { err := recover() if err == nil { return } var msg string switch err := err.(type) { case string: msg = err case error: if !errors.Is(err, context.Canceled) && len(utils.CmdSuggestion) == 0 && !viper.GetBool("DEBUG") { utils.CmdSuggestion = utils.SuggestDebugFlag } msg = err.Error() default: msg = fmt.Sprintf("%#v", err) } // Log error to console fmt.Fprintln(os.Stderr, utils.Red(msg)) if len(utils.CmdSuggestion) > 0 { fmt.Fprintln(os.Stderr, utils.CmdSuggestion) } // Report error to sentry if createTicket && len(utils.SentryDsn) > 0 { sentry.ConfigureScope(addSentryScope) eventId := sentry.CurrentHub().Recover(err) if eventId != nil && sentry.Flush(2*time.Second) { fmt.Fprintln(os.Stderr, "Sent crash report:", *eventId) fmt.Fprintln(os.Stderr, "Quote the crash ID above when filing a bug report: https://github.com/supabase/cli/issues/new/choose") } } os.Exit(1) } func init() { cobra.OnInitialize(func() { viper.SetEnvPrefix("SUPABASE") viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) viper.AutomaticEnv() }) flags := rootCmd.PersistentFlags() flags.Bool("debug", false, "output debug logs to stderr") flags.String("workdir", "", "path to a Supabase project directory") flags.Bool("experimental", false, "enable experimental features") flags.String("network-id", "", "use the specified docker network instead of a generated one") flags.Var(&utils.OutputFormat, "output", "output format of status variables") flags.Var(&utils.DNSResolver, "dns-resolver", "lookup domain names using the specified resolver") flags.BoolVar(&createTicket, "create-ticket", false, "create a support ticket for any CLI error") cobra.CheckErr(viper.BindPFlags(flags)) rootCmd.SetVersionTemplate("{{.Version}}\n") rootCmd.AddGroup(&cobra.Group{ID: groupQuickStart, Title: "Quick Start:"}) rootCmd.AddGroup(&cobra.Group{ID: groupLocalDev, Title: "Local Development:"}) rootCmd.AddGroup(&cobra.Group{ID: groupManagementAPI, Title: "Management APIs:"}) } // instantiate new rootCmd is a bit tricky with cobra, but it can be done later with the following // approach for example: https://github.com/portworx/pxc/tree/master/cmd func GetRootCmd() *cobra.Command { return rootCmd } func addSentryScope(scope *sentry.Scope) { serviceImages := utils.Config.GetServiceImages() imageToVersion := make(map[string]interface{}, len(serviceImages)) for _, image := range serviceImages { parts := strings.Split(image, ":") // Bypasses sentry's IP sanitization rule, ie. 15.1.0.147 if net.ParseIP(parts[1]) != nil { imageToVersion[parts[0]] = "v" + parts[1] } else { imageToVersion[parts[0]] = parts[1] } } scope.SetContext("Services", imageToVersion) scope.SetContext("Config", map[string]interface{}{ "Image Registry": utils.GetRegistry(), "Project ID": flags.ProjectRef, }) }