diff --git a/cmd/start.go b/cmd/start.go index bd1cc1f8a..41815d4cd 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -40,6 +40,7 @@ var ( excludedContainers []string ignoreHealthCheck bool preview bool + foreground bool startCmd = &cobra.Command{ GroupID: groupLocalDev, @@ -47,7 +48,7 @@ var ( Short: "Start containers for Supabase local development", RunE: func(cmd *cobra.Command, args []string) error { validateExcludedContainers(excludedContainers) - return start.Run(cmd.Context(), afero.NewOsFs(), excludedContainers, ignoreHealthCheck) + return start.Run(cmd.Context(), afero.NewOsFs(), excludedContainers, ignoreHealthCheck, foreground) }, } ) @@ -58,6 +59,7 @@ func init() { flags.StringSliceVarP(&excludedContainers, "exclude", "x", []string{}, "Names of containers to not start. ["+names+"]") flags.BoolVar(&ignoreHealthCheck, "ignore-health-check", false, "Ignore unhealthy services and exit 0") flags.BoolVar(&preview, "preview", false, "Connect to feature preview branch") + flags.BoolVarP(&foreground, "foreground", "f", false, "Run in foreground and stop services on exit") cobra.CheckErr(flags.MarkHidden("preview")) rootCmd.AddCommand(startCmd) } diff --git a/docs/supabase/start.md b/docs/supabase/start.md index a590610f1..1d74e1dbe 100644 --- a/docs/supabase/start.md +++ b/docs/supabase/start.md @@ -9,3 +9,5 @@ All service containers are started by default. You can exclude those not needed > It is recommended to have at least 7GB of RAM to start all services. Health checks are automatically added to verify the started containers. Use `--ignore-health-check` flag to ignore these errors. + +By default, `supabase start` starts the services and exits. Use `-f` or `--foreground` flag to run in foreground mode, which keeps the command running and automatically stops services when the terminal is closed or interrupted. diff --git a/internal/start/start.go b/internal/start/start.go index bcc2cde48..f453cddce 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -9,11 +9,13 @@ import ( "net" "net/url" "os" + "os/signal" "path" "path/filepath" "slices" "strconv" "strings" + "syscall" "text/template" "time" @@ -38,12 +40,13 @@ import ( "github.com/supabase/cli/internal/seed/buckets" "github.com/supabase/cli/internal/services" "github.com/supabase/cli/internal/status" + "github.com/supabase/cli/internal/stop" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/config" ) -func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error { +func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool, foreground bool) error { // Sanity checks. { if err := flags.LoadConfig(fsys); err != nil { @@ -81,6 +84,10 @@ func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignore fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase")) status.PrettyPrint(os.Stdout, excludedContainers...) + + if foreground { + return runForeground(ctx, fsys) + } return nil } @@ -1304,6 +1311,22 @@ func ExcludableContainers() []string { return names } +func runForeground(ctx context.Context, fsys afero.Fs) error { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + + fmt.Fprintln(os.Stderr, "Running in foreground. Press Ctrl+C to stop services.") + + select { + case sig := <-sigChan: + fmt.Fprintf(os.Stderr, "\nReceived signal %v, stopping services...\n", sig) + case <-ctx.Done(): + fmt.Fprintln(os.Stderr, "\nContext cancelled, stopping services...") + } + + return stop.Run(ctx, true, "", false, fsys) // backup=true, no specific project, not all +} + func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) { numOfKeyPairs := len(input) i := 0 diff --git a/internal/start/start_test.go b/internal/start/start_test.go index a90d7a719..c3f820543 100644 --- a/internal/start/start_test.go +++ b/internal/start/start_test.go @@ -31,7 +31,7 @@ func TestStartCommand(t *testing.T) { fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644)) // Run test - err := Run(context.Background(), fsys, []string{}, false) + err := Run(context.Background(), fsys, []string{}, false, false) // Check error assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there") }) @@ -47,7 +47,7 @@ func TestStartCommand(t *testing.T) { Get("/v" + utils.Docker.ClientVersion() + "/containers"). ReplyError(errors.New("network error")) // Run test - err := Run(context.Background(), fsys, []string{}, false) + err := Run(context.Background(), fsys, []string{}, false, false) // Check error assert.ErrorContains(t, err, "network error") assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -84,7 +84,7 @@ func TestStartCommand(t *testing.T) { Reply(http.StatusOK). JSON(running) // Run test - err := Run(context.Background(), fsys, []string{}, false) + err := Run(context.Background(), fsys, []string{}, false, false) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests())