• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 21442698789

28 Jan 2026 02:43PM UTC coverage: 51.225% (-5.0%) from 56.176%
21442698789

Pull #4770

github

web-flow
Merge 0b92817a1 into 3fe548f00
Pull Request #4770: feat: add `supabase dev` command

8 of 1187 new or added lines in 10 files covered. (0.67%)

5 existing lines in 1 file now uncovered.

6857 of 13386 relevant lines covered (51.23%)

5.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/internal/dev/dev.go
1
package dev
2

3
import (
4
        "context"
5
        "fmt"
6
        "os"
7
        "os/exec"
8
        "os/signal"
9
        "path/filepath"
10
        "strings"
11
        "syscall"
12

13
        "github.com/go-errors/errors"
14
        "github.com/spf13/afero"
15
        "github.com/supabase/cli/internal/start"
16
        "github.com/supabase/cli/internal/utils"
17
        "github.com/supabase/cli/internal/utils/flags"
18
)
19

20
// Run starts the dev session
NEW
21
func Run(ctx context.Context, fsys afero.Fs) error {
×
NEW
22
        // Load config first
×
NEW
23
        if err := flags.LoadConfig(fsys); err != nil {
×
NEW
24
                return err
×
NEW
25
        }
×
26

27
        // Ensure local database is running
NEW
28
        if err := ensureDbRunning(ctx, fsys); err != nil {
×
NEW
29
                return err
×
NEW
30
        }
×
31

32
        // Create and run the dev session
NEW
33
        session := NewSession(ctx, fsys)
×
NEW
34
        return session.Run()
×
35
}
36

37
// ensureDbRunning starts the local database if it's not already running
NEW
38
func ensureDbRunning(ctx context.Context, fsys afero.Fs) error {
×
NEW
39
        if err := utils.AssertSupabaseDbIsRunning(); err == nil {
×
NEW
40
                fmt.Fprintln(os.Stderr, "Using existing local database")
×
NEW
41
                return nil
×
NEW
42
        } else if !errors.Is(err, utils.ErrNotRunning) {
×
NEW
43
                return err
×
NEW
44
        }
×
45

NEW
46
        fmt.Fprintln(os.Stderr, "Starting local database...")
×
NEW
47
        return start.Run(ctx, fsys, nil, false)
×
48
}
49

50
// Session manages the dev mode lifecycle
51
type Session struct {
52
        ctx    context.Context
53
        cancel context.CancelFunc
54
        fsys   afero.Fs
55
        dirty  bool // tracks whether schema changes have been applied
56
}
57

58
// NewSession creates a new dev session
NEW
59
func NewSession(ctx context.Context, fsys afero.Fs) *Session {
×
NEW
60
        ctx, cancel := context.WithCancel(ctx)
×
NEW
61
        return &Session{
×
NEW
62
                ctx:    ctx,
×
NEW
63
                cancel: cancel,
×
NEW
64
                fsys:   fsys,
×
NEW
65
                dirty:  false,
×
NEW
66
        }
×
NEW
67
}
×
68

69
// Run starts the dev session main loop
NEW
70
func (s *Session) Run() error {
×
NEW
71
        schemasConfig := &utils.Config.Dev.Schemas
×
NEW
72

×
NEW
73
        // Check if schemas workflow is enabled
×
NEW
74
        if !schemasConfig.IsEnabled() {
×
NEW
75
                fmt.Fprintln(os.Stderr, "[dev] Schema workflow is disabled in config")
×
NEW
76
                fmt.Fprintln(os.Stderr, "[dev] Press Ctrl+C to stop")
×
NEW
77

×
NEW
78
                // Set up signal handling for graceful shutdown
×
NEW
79
                sigCh := make(chan os.Signal, 1)
×
NEW
80
                signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
×
NEW
81
                <-sigCh
×
NEW
82
                fmt.Fprintln(os.Stderr)
×
NEW
83
                fmt.Fprintln(os.Stderr, "[dev] Stopping dev session...")
×
NEW
84
                return nil
×
NEW
85
        }
×
86

87
        // Get watch globs from config
NEW
88
        watchGlobs := schemasConfig.Watch
×
NEW
89
        if len(watchGlobs) == 0 {
×
NEW
90
                // Fallback to default if not configured
×
NEW
91
                watchGlobs = []string{"schemas/**/*.sql"}
×
NEW
92
        }
×
93

94
        // Validate config on startup
NEW
95
        s.validateConfig()
×
NEW
96

×
NEW
97
        // Create schemas directory if using default pattern and it doesn't exist
×
NEW
98
        if exists, err := afero.DirExists(s.fsys, utils.SchemasDir); err != nil {
×
NEW
99
                return errors.Errorf("failed to check schemas directory: %w", err)
×
NEW
100
        } else if !exists {
×
NEW
101
                fmt.Fprintf(os.Stderr, "[dev] Creating %s directory...\n", utils.Aqua(utils.SchemasDir))
×
NEW
102
                if err := s.fsys.MkdirAll(utils.SchemasDir, 0755); err != nil {
×
NEW
103
                        return errors.Errorf("failed to create schemas directory: %w", err)
×
NEW
104
                }
×
105
        }
106

107
        // Set up signal handling
NEW
108
        sigCh := make(chan os.Signal, 1)
×
NEW
109
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
×
NEW
110

×
NEW
111
        // Get seed globs from [db.seed] config (already resolved to absolute paths)
×
NEW
112
        seedConfig := &utils.Config.Dev.Seed
×
NEW
113
        var seedGlobs []string
×
NEW
114
        if seedConfig.IsEnabled() && utils.Config.Db.Seed.Enabled {
×
NEW
115
                seedGlobs = utils.Config.Db.Seed.SqlPaths
×
NEW
116
        }
×
117

118
        // Display configuration
NEW
119
        fmt.Fprintln(os.Stderr)
×
NEW
120
        fmt.Fprintf(os.Stderr, "[dev] Watching: %v\n", watchGlobs)
×
NEW
121
        if schemasConfig.OnChange != "" {
×
NEW
122
                fmt.Fprintf(os.Stderr, "[dev] On change: %s\n", utils.Aqua(schemasConfig.OnChange))
×
NEW
123
        } else {
×
NEW
124
                fmt.Fprintf(os.Stderr, "[dev] On change: %s\n", utils.Aqua("(internal differ)"))
×
NEW
125
        }
×
NEW
126
        if schemasConfig.Types != "" {
×
NEW
127
                fmt.Fprintf(os.Stderr, "[dev] Types output: %s\n", utils.Aqua(schemasConfig.Types))
×
NEW
128
        }
×
NEW
129
        if seedConfig.IsEnabled() {
×
NEW
130
                if seedConfig.OnChange != "" {
×
NEW
131
                        fmt.Fprintf(os.Stderr, "[dev] Seed: %s\n", utils.Aqua(seedConfig.OnChange))
×
NEW
132
                } else if utils.Config.Db.Seed.Enabled && len(utils.Config.Db.Seed.SqlPaths) > 0 {
×
NEW
133
                        fmt.Fprintf(os.Stderr, "[dev] Seed: %s\n", utils.Aqua("(internal)"))
×
NEW
134
                }
×
135
        }
NEW
136
        fmt.Fprintln(os.Stderr, "[dev] Press Ctrl+C to stop")
×
NEW
137
        fmt.Fprintln(os.Stderr)
×
NEW
138

×
NEW
139
        // Create the schema watcher
×
NEW
140
        watcher, err := NewSchemaWatcher(s.fsys, watchGlobs, seedGlobs)
×
NEW
141
        if err != nil {
×
NEW
142
                return err
×
NEW
143
        }
×
NEW
144
        defer watcher.Close()
×
NEW
145

×
NEW
146
        // Start the watcher
×
NEW
147
        go watcher.Start()
×
NEW
148

×
NEW
149
        // Apply initial schema state
×
NEW
150
        fmt.Fprintln(os.Stderr, "[dev] Applying initial schema state...")
×
NEW
151
        if err := s.applySchemaChanges(); err != nil {
×
NEW
152
                fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
153
        } else {
×
NEW
154
                fmt.Fprintln(os.Stderr, "[dev] Initial sync complete")
×
NEW
155
        }
×
156

157
        // Run initial seed (after schema sync)
NEW
158
        if seedConfig.IsEnabled() {
×
NEW
159
                fmt.Fprintln(os.Stderr, "[dev] Running initial seed...")
×
NEW
160
                if err := s.runSeed(); err != nil {
×
NEW
161
                        fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
162
                } else {
×
NEW
163
                        fmt.Fprintln(os.Stderr, "[dev] Initial seed complete")
×
NEW
164
                }
×
165
        }
166

NEW
167
        fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
168

×
NEW
169
        // Main event loop
×
NEW
170
        for {
×
NEW
171
                select {
×
NEW
172
                case <-s.ctx.Done():
×
NEW
173
                        CleanupShadow(s.ctx)
×
NEW
174
                        return s.ctx.Err()
×
NEW
175
                case <-sigCh:
×
NEW
176
                        fmt.Fprintln(os.Stderr)
×
NEW
177
                        fmt.Fprintln(os.Stderr, "[dev] Stopping dev session...")
×
NEW
178
                        CleanupShadow(s.ctx)
×
NEW
179
                        s.showDirtyWarning()
×
NEW
180
                        return nil
×
NEW
181
                case <-watcher.RestartCh:
×
NEW
182
                        // Check if seeds changed - if so, reseed the database
×
NEW
183
                        if watcher.SeedsChanged() {
×
NEW
184
                                fmt.Fprintln(os.Stderr, "[dev] Reseeding database...")
×
NEW
185
                                if err := s.runSeed(); err != nil {
×
NEW
186
                                        fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Red("Error:"), err.Error())
×
NEW
187
                                } else {
×
NEW
188
                                        fmt.Fprintln(os.Stderr, "[dev] Reseed complete")
×
NEW
189
                                }
×
NEW
190
                                fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
191
                                continue
×
192
                        }
193

194
                        // Check if migrations changed - if so, just invalidate the shadow template
195
                        // We do NOT auto-apply migrations because:
196
                        // 1. If created by `db diff -f`, local DB already has those changes
197
                        // 2. If from external source (git pull), user should restart dev mode
NEW
198
                        if watcher.MigrationsChanged() {
×
NEW
199
                                fmt.Fprintln(os.Stderr, "[dev] Migration file changed - shadow template invalidated")
×
NEW
200
                                InvalidateShadowTemplate()
×
NEW
201
                                // Don't trigger schema diff - migrations need manual application
×
NEW
202
                                // The next schema file change will use the updated shadow
×
NEW
203
                                fmt.Fprintln(os.Stderr, "[dev] Note: Run 'supabase db reset' or restart dev mode to apply new migrations")
×
NEW
204
                                fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
205
                                continue
×
206
                        }
207

NEW
208
                        fmt.Fprintln(os.Stderr, "[dev] Applying schema changes...")
×
NEW
209
                        if err := s.applySchemaChanges(); err != nil {
×
NEW
210
                                fmt.Fprintf(os.Stderr, "[dev] %s %s\n", utils.Red("Error:"), err.Error())
×
NEW
211
                        } else {
×
NEW
212
                                fmt.Fprintln(os.Stderr, "[dev] Changes applied successfully")
×
NEW
213
                        }
×
NEW
214
                        fmt.Fprintln(os.Stderr, "[dev] Watching for changes...")
×
NEW
215
                case err := <-watcher.ErrCh:
×
NEW
216
                        CleanupShadow(s.ctx)
×
NEW
217
                        return errors.Errorf("watcher error: %w", err)
×
218
                }
219
        }
220
}
221

222
// validateConfig checks the configuration and warns about potential issues
NEW
223
func (s *Session) validateConfig() {
×
NEW
224
        schemasCfg := &utils.Config.Dev.Schemas
×
NEW
225
        seedCfg := &utils.Config.Dev.Seed
×
NEW
226

×
NEW
227
        // Warn if schema on_change command might not exist
×
NEW
228
        if schemasCfg.OnChange != "" {
×
NEW
229
                s.validateOnChangeCommand(schemasCfg.OnChange, "schemas")
×
NEW
230
        }
×
231

232
        // Warn if seed on_change command might not exist
NEW
233
        if seedCfg.OnChange != "" {
×
NEW
234
                s.validateOnChangeCommand(seedCfg.OnChange, "seed")
×
NEW
235
        }
×
236

237
        // Warn if types output directory doesn't exist
NEW
238
        if schemasCfg.Types != "" {
×
NEW
239
                dir := filepath.Dir(schemasCfg.Types)
×
NEW
240
                if dir != "." && dir != "" {
×
NEW
241
                        if exists, _ := afero.DirExists(s.fsys, dir); !exists {
×
NEW
242
                                fmt.Fprintf(os.Stderr, "[dev] %s types output directory '%s' does not exist\n", utils.Yellow("Warning:"), dir)
×
NEW
243
                        }
×
244
                }
245
        }
246
}
247

248
// validateOnChangeCommand checks if the on_change command exists
NEW
249
func (s *Session) validateOnChangeCommand(command, configSection string) {
×
NEW
250
        cmdParts := strings.Fields(command)
×
NEW
251
        if len(cmdParts) > 0 {
×
NEW
252
                cmdName := cmdParts[0]
×
NEW
253
                // Check if it's a known package manager command
×
NEW
254
                if cmdName != "npx" && cmdName != "npm" && cmdName != "yarn" && cmdName != "pnpm" && cmdName != "bunx" {
×
NEW
255
                        if _, err := exec.LookPath(cmdName); err != nil {
×
NEW
256
                                fmt.Fprintf(os.Stderr, "[dev] %s %s on_change command '%s' not found in PATH\n", utils.Yellow("Warning:"), configSection, cmdName)
×
NEW
257
                        }
×
258
                }
259
        }
260
}
261

262
// applySchemaChanges validates and applies schema changes to the local database
NEW
263
func (s *Session) applySchemaChanges() error {
×
NEW
264
        schemasConfig := &utils.Config.Dev.Schemas
×
NEW
265

×
NEW
266
        // Step 0: Verify DB is still running
×
NEW
267
        if err := utils.AssertSupabaseDbIsRunning(); err != nil {
×
NEW
268
                return errors.Errorf("local database stopped unexpectedly: %w", err)
×
NEW
269
        }
×
270

271
        // Check if we should use a custom on_change command
NEW
272
        if schemasConfig.OnChange != "" {
×
NEW
273
                return s.runCustomOnChange(schemasConfig.OnChange)
×
NEW
274
        }
×
275

276
        // Step 1: Load all schema files
NEW
277
        schemaFiles, err := loadSchemaFiles(s.fsys)
×
NEW
278
        if err != nil {
×
NEW
279
                return err
×
NEW
280
        }
×
281

NEW
282
        if len(schemaFiles) == 0 {
×
NEW
283
                fmt.Fprintln(os.Stderr, "No schema files found")
×
NEW
284
                return nil
×
NEW
285
        }
×
286

287
        // Step 2: Validate SQL syntax of all schema files
NEW
288
        if err := ValidateSchemaFiles(schemaFiles, s.fsys); err != nil {
×
NEW
289
                return err
×
NEW
290
        }
×
291

292
        // Step 3: Run diff and apply changes
NEW
293
        if err := s.diffAndApply(); err != nil {
×
NEW
294
                return err
×
NEW
295
        }
×
296

297
        // Step 4: Generate types if configured
NEW
298
        if schemasConfig.Types != "" {
×
NEW
299
                if err := s.generateTypes(schemasConfig.Types); err != nil {
×
NEW
300
                        fmt.Fprintf(os.Stderr, "%s Failed to generate types: %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
301
                }
×
302
        }
303

NEW
304
        return nil
×
305
}
306

307
// runCustomOnChange executes a custom command when files change
NEW
308
func (s *Session) runCustomOnChange(command string) error {
×
NEW
309
        fmt.Fprintf(os.Stderr, "Running: %s\n", utils.Aqua(command))
×
NEW
310

×
NEW
311
        cmd := exec.CommandContext(s.ctx, "sh", "-c", command)
×
NEW
312
        cmd.Stdout = os.Stdout
×
NEW
313
        cmd.Stderr = os.Stderr
×
NEW
314
        cmd.Dir = utils.CurrentDirAbs
×
NEW
315

×
NEW
316
        if err := cmd.Run(); err != nil {
×
NEW
317
                return errors.Errorf("on_change command failed: %w", err)
×
NEW
318
        }
×
319

NEW
320
        s.dirty = true
×
NEW
321

×
NEW
322
        // Generate types if configured (runs after custom command too)
×
NEW
323
        schemasConfig := &utils.Config.Dev.Schemas
×
NEW
324
        if schemasConfig.Types != "" {
×
NEW
325
                if err := s.generateTypes(schemasConfig.Types); err != nil {
×
NEW
326
                        fmt.Fprintf(os.Stderr, "%s Failed to generate types: %s\n", utils.Yellow("Warning:"), err.Error())
×
NEW
327
                }
×
328
        }
329

NEW
330
        return nil
×
331
}
332

333
// generateTypes generates TypeScript types and writes them to the configured path
NEW
334
func (s *Session) generateTypes(outputPath string) error {
×
NEW
335
        fmt.Fprintf(os.Stderr, "Generating types to %s...\n", utils.Aqua(outputPath))
×
NEW
336

×
NEW
337
        // Run supabase gen types typescript --local
×
NEW
338
        cmd := exec.CommandContext(s.ctx, "supabase", "gen", "types", "typescript", "--local")
×
NEW
339
        output, err := cmd.Output()
×
NEW
340
        if err != nil {
×
NEW
341
                if exitErr, ok := err.(*exec.ExitError); ok {
×
NEW
342
                        return errors.Errorf("type generation failed: %s", string(exitErr.Stderr))
×
NEW
343
                }
×
NEW
344
                return errors.Errorf("type generation failed: %w", err)
×
345
        }
346

347
        // Write output to file
NEW
348
        if err := afero.WriteFile(s.fsys, outputPath, output, 0644); err != nil {
×
NEW
349
                return errors.Errorf("failed to write types file: %w", err)
×
NEW
350
        }
×
351

NEW
352
        fmt.Fprintf(os.Stderr, "Types generated: %s\n", utils.Aqua(outputPath))
×
NEW
353
        return nil
×
354
}
355

356
// diffAndApply runs the schema diff and applies changes to local DB
NEW
357
func (s *Session) diffAndApply() error {
×
NEW
358
        applied, err := DiffAndApply(s.ctx, s.fsys, os.Stderr)
×
NEW
359
        if err != nil {
×
NEW
360
                return err
×
NEW
361
        }
×
NEW
362
        if applied {
×
NEW
363
                s.dirty = true
×
NEW
364
        }
×
NEW
365
        return nil
×
366
}
367

368
// showDirtyWarning warns if local DB has uncommitted schema changes
NEW
369
func (s *Session) showDirtyWarning() {
×
NEW
370
        if !s.dirty {
×
NEW
371
                return
×
NEW
372
        }
×
NEW
373
        fmt.Fprintf(os.Stderr, "%s Local database has uncommitted schema changes!\n", utils.Yellow("Warning:"))
×
NEW
374
        fmt.Fprintf(os.Stderr, "    Run '%s' to create a migration\n", utils.Aqua("supabase db diff -f migration_name"))
×
375
}
376

377
// loadSchemaFiles returns all .sql files in the schemas directory
NEW
378
func loadSchemaFiles(fsys afero.Fs) ([]string, error) {
×
NEW
379
        var files []string
×
NEW
380
        err := afero.Walk(fsys, utils.SchemasDir, func(path string, info os.FileInfo, err error) error {
×
NEW
381
                if err != nil {
×
NEW
382
                        return err
×
NEW
383
                }
×
NEW
384
                if info.Mode().IsRegular() && len(path) > 4 && path[len(path)-4:] == ".sql" {
×
NEW
385
                        files = append(files, path)
×
NEW
386
                }
×
NEW
387
                return nil
×
388
        })
NEW
389
        return files, err
×
390
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc