diff --git a/internal/cli/rollback.go b/internal/cli/rollback.go new file mode 100644 index 0000000..bdc7e9d --- /dev/null +++ b/internal/cli/rollback.go @@ -0,0 +1,21 @@ +package cli + +import ( + "context" + "fmt" + + "github.com/b-jonathan/taco/internal/stacks" +) + +func rollbackStacks(ctx context.Context, opts *stacks.Options, ss ...stacks.Stack) { + for _, s := range ss { + if s == nil { + continue + } + fmt.Printf("Rolling back %s stack...\n", s.Name()) + + if err := s.Rollback(ctx, opts); err != nil { + fmt.Printf("rollback %s failed: %v\n", s.Name(), err) + } + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go index c94c0fe..5f00fe5 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -205,6 +205,19 @@ func initCmd() *cobra.Command { Port: 4000, } + // Rollback logic + rollbackNeeded := true + defer func() { + if !rollbackNeeded { + return + } + fmt.Println("Init failed, starting rollback...") + // use a fresh context for rollback so it isn't canceled + rbCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + rollbackStacks(rbCtx, opts, frontend, backend, database, auth) + }() + // This is core core g, ctx := errgroup.WithContext(rootCtx) @@ -281,6 +294,7 @@ func initCmd() *cobra.Command { fmt.Println("Pushed:", repo.GetHTMLURL()) } + rollbackNeeded = false fmt.Println("Time Taken:", time.Since(start)) return nil }, diff --git a/internal/fsutil/fsutil.go b/internal/fsutil/fsutil.go index 545e92c..25d794a 100644 --- a/internal/fsutil/fsutil.go +++ b/internal/fsutil/fsutil.go @@ -84,6 +84,10 @@ func WithFileLock(path string, fn func() error) error { return fn() } +func RemoveDir(path string) error { + return os.RemoveAll(path) +} + func RenderTemplate(tmplPath string) ([]byte, error) { data, err := templates.FS.ReadFile(tmplPath) if err != nil { diff --git a/internal/stacks/express/express.go b/internal/stacks/express/express.go index 40ba0c7..1d73259 100644 --- a/internal/stacks/express/express.go +++ b/internal/stacks/express/express.go @@ -115,3 +115,13 @@ FRONTEND_ORIGIN=http://localhost:3000` return nil } + +func (express) Rollback(ctx context.Context, opts *Options) error { + backendDir := filepath.Join(opts.ProjectRoot, "backend") + + if err := fsutil.RemoveDir(backendDir); err != nil { + return fmt.Errorf("remove backend dir: %w", err) + } + + return nil +} diff --git a/internal/stacks/firebase/firebase.go b/internal/stacks/firebase/firebase.go index 44b3946..d6ee02f 100644 --- a/internal/stacks/firebase/firebase.go +++ b/internal/stacks/firebase/firebase.go @@ -176,3 +176,7 @@ func (firebase) Post(ctx context.Context, opts *Options) error { fmt.Println("Firebase post-generation complete. Added Firebase ignores and credentials.") return nil } + +func (firebase) Rollback(ctx context.Context, opts *Options) error { + return nil +} diff --git a/internal/stacks/mongodb/mongodb.go b/internal/stacks/mongodb/mongodb.go index 8a53fa1..7c28bb2 100644 --- a/internal/stacks/mongodb/mongodb.go +++ b/internal/stacks/mongodb/mongodb.go @@ -2,6 +2,7 @@ package mongodb import ( "context" + "errors" "fmt" "path/filepath" "strings" @@ -198,3 +199,29 @@ func (mongodb) Post(ctx context.Context, opts *Options) error { _ = fsutil.AppendUniqueLines(path, []string{content}) return nil } + +func (mongodb) Rollback(ctx context.Context, opts *Options) error { + if opts.DatabaseURI == "" { + return nil + } + client, err := mongo.Connect(ctx, options.Client().ApplyURI(opts.DatabaseURI)) + if err != nil { + return fmt.Errorf("connect mongo for rollback: %w", err) + } + defer func() { _ = client.Disconnect(ctx) }() + + db := client.Database(opts.AppName) + col := db.Collection("seed_test") + + if err := col.Drop(ctx); err != nil { + var cmdErr *mongo.CommandError + // Code 26 = NamespaceNotFound (collection doesn't exist) — safe to ignore + if errors.As(err, &cmdErr) && cmdErr.Code == 26 { + return nil + } + return fmt.Errorf("drop seed_test collection: %w", err) + } + + fmt.Println("Rolled back MongoDB seed data (dropped collection seed_test)") + return nil +} diff --git a/internal/stacks/nextjs/nextjs.go b/internal/stacks/nextjs/nextjs.go index 9aafb79..12360a1 100644 --- a/internal/stacks/nextjs/nextjs.go +++ b/internal/stacks/nextjs/nextjs.go @@ -114,3 +114,13 @@ func (nextjs) Post(ctx context.Context, opts *Options) error { return nil } + +func (nextjs) Rollback(ctx context.Context, opts *Options) error { + frontendDir := filepath.Join(opts.ProjectRoot, "frontend") + + if err := fsutil.RemoveDir(frontendDir); err != nil { + return fmt.Errorf("remove frontend dir: %w", err) + } + + return nil +} diff --git a/internal/stacks/types.go b/internal/stacks/types.go index 9484264..7d41802 100644 --- a/internal/stacks/types.go +++ b/internal/stacks/types.go @@ -8,6 +8,7 @@ type Stack interface { Init(ctx context.Context, opts *Options) error Generate(ctx context.Context, opts *Options) error Post(ctx context.Context, opts *Options) error + Rollback(ctx context.Context, opts *Options) error } type Seeder interface {