a love letter to tangled (android, iOS, and a search API)

feat: local dev flag

+113 -22
+2
.gitignore
··· 31 31 platforms 32 32 plugins 33 33 www 34 + 35 + *.db
+27 -1
packages/api/internal/config/config.go
··· 35 35 AdminAuthToken string 36 36 } 37 37 38 - func Load() (*Config, error) { 38 + type LoadOptions struct { 39 + Local bool 40 + WorkDir string 41 + } 42 + 43 + func Load(opts LoadOptions) (*Config, error) { 39 44 loadDotEnv() 40 45 41 46 cfg := &Config{ ··· 63 68 EnableAdminEndpoints: envBool("ENABLE_ADMIN_ENDPOINTS", false), 64 69 } 65 70 71 + if opts.Local { 72 + dbURL, err := localDatabaseURL(opts.WorkDir) 73 + if err != nil { 74 + return nil, err 75 + } 76 + cfg.TursoURL = dbURL 77 + cfg.TursoToken = "" 78 + cfg.LogFormat = "text" 79 + } 80 + 66 81 var errs []error 67 82 if cfg.TursoURL == "" { 68 83 errs = append(errs, errors.New("TURSO_DATABASE_URL is required")) ··· 74 89 return nil, errors.Join(errs...) 75 90 } 76 91 return cfg, nil 92 + } 93 + 94 + func localDatabaseURL(workDir string) (string, error) { 95 + if strings.TrimSpace(workDir) == "" { 96 + var err error 97 + workDir, err = os.Getwd() 98 + if err != nil { 99 + return "", err 100 + } 101 + } 102 + return "file:" + filepath.Join(workDir, "twister-dev.db"), nil 77 103 } 78 104 79 105 func loadDotEnv() {
+59
packages/api/internal/config/config_test.go
··· 1 + package config 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + ) 8 + 9 + func TestLoadRequiresRemoteTursoConfigurationByDefault(t *testing.T) { 10 + t.Setenv("TURSO_DATABASE_URL", "") 11 + t.Setenv("TURSO_AUTH_TOKEN", "") 12 + 13 + _, err := Load(LoadOptions{}) 14 + if err == nil { 15 + t.Fatal("expected missing Turso config to fail") 16 + } 17 + } 18 + 19 + func TestLoadLocalOverridesRemoteDatabaseAndLogging(t *testing.T) { 20 + workDir := t.TempDir() 21 + t.Setenv("TURSO_DATABASE_URL", "libsql://example.turso.io") 22 + t.Setenv("TURSO_AUTH_TOKEN", "secret") 23 + t.Setenv("LOG_FORMAT", "json") 24 + 25 + cfg, err := Load(LoadOptions{Local: true, WorkDir: workDir}) 26 + if err != nil { 27 + t.Fatalf("load config: %v", err) 28 + } 29 + 30 + wantURL := "file:" + filepath.Join(workDir, "twister-dev.db") 31 + if cfg.TursoURL != wantURL { 32 + t.Fatalf("TursoURL: got %q, want %q", cfg.TursoURL, wantURL) 33 + } 34 + if cfg.TursoToken != "" { 35 + t.Fatalf("TursoToken: got %q, want empty", cfg.TursoToken) 36 + } 37 + if cfg.LogFormat != "text" { 38 + t.Fatalf("LogFormat: got %q, want %q", cfg.LogFormat, "text") 39 + } 40 + } 41 + 42 + func TestLoadLocalUsesCurrentWorkingDirectoryWhenUnset(t *testing.T) { 43 + wd, err := os.Getwd() 44 + if err != nil { 45 + t.Fatalf("getwd: %v", err) 46 + } 47 + t.Setenv("TURSO_DATABASE_URL", "") 48 + t.Setenv("TURSO_AUTH_TOKEN", "") 49 + 50 + cfg, err := Load(LoadOptions{Local: true}) 51 + if err != nil { 52 + t.Fatalf("load config: %v", err) 53 + } 54 + 55 + wantURL := "file:" + filepath.Join(wd, "twister-dev.db") 56 + if cfg.TursoURL != wantURL { 57 + t.Fatalf("TursoURL: got %q, want %q", cfg.TursoURL, wantURL) 58 + } 59 + }
+25 -21
packages/api/main.go
··· 28 28 ) 29 29 30 30 func main() { 31 + var local bool 32 + 31 33 root := &cobra.Command{ 32 34 Use: "twister", 33 35 Short: "Tangled search service", ··· 36 38 SilenceErrors: true, 37 39 } 38 40 41 + root.PersistentFlags().BoolVar(&local, "local", false, "Use a local twister-dev.db database and text logs for development") 42 + 39 43 root.AddCommand( 40 - newAPICmd(), 41 - newIndexerCmd(), 42 - newBackfillCmd(), 43 - newEmbedWorkerCmd(), 44 - newReindexCmd(), 45 - newReembedCmd(), 46 - newHealthcheckCmd(), 44 + newAPICmd(&local), 45 + newIndexerCmd(&local), 46 + newBackfillCmd(&local), 47 + newEmbedWorkerCmd(&local), 48 + newReindexCmd(&local), 49 + newReembedCmd(&local), 50 + newHealthcheckCmd(&local), 47 51 ) 48 52 49 53 if err := root.Execute(); err != nil { ··· 63 67 return ctx, cancel 64 68 } 65 69 66 - func newAPICmd() *cobra.Command { 70 + func newAPICmd(local *bool) *cobra.Command { 67 71 return &cobra.Command{ 68 72 Use: "api", 69 73 Aliases: []string{"serve"}, 70 74 Short: "Start the HTTP search API", 71 75 RunE: func(cmd *cobra.Command, args []string) error { 72 - cfg, err := config.Load() 76 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 73 77 if err != nil { 74 78 return fmt.Errorf("config: %w", err) 75 79 } ··· 103 107 } 104 108 } 105 109 106 - func newIndexerCmd() *cobra.Command { 110 + func newIndexerCmd(local *bool) *cobra.Command { 107 111 return &cobra.Command{ 108 112 Use: "indexer", 109 113 Short: "Start the Tap consumer and indexer", 110 114 RunE: func(cmd *cobra.Command, args []string) error { 111 - cfg, err := config.Load() 115 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 112 116 if err != nil { 113 117 return fmt.Errorf("config: %w", err) 114 118 } ··· 176 180 } 177 181 } 178 182 179 - func newEmbedWorkerCmd() *cobra.Command { 183 + func newEmbedWorkerCmd(local *bool) *cobra.Command { 180 184 return &cobra.Command{ 181 185 Use: "embed-worker", 182 186 Short: "Start the async embedding worker", 183 187 RunE: func(cmd *cobra.Command, args []string) error { 184 - cfg, err := config.Load() 188 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 185 189 if err != nil { 186 190 return fmt.Errorf("config: %w", err) 187 191 } ··· 196 200 } 197 201 } 198 202 199 - func newBackfillCmd() *cobra.Command { 203 + func newBackfillCmd(local *bool) *cobra.Command { 200 204 var opts backfill.Options 201 205 202 206 cmd := &cobra.Command{ 203 207 Use: "backfill", 204 208 Short: "Discover users from seeds and register repos for Tap backfill", 205 209 RunE: func(cmd *cobra.Command, args []string) error { 206 - cfg, err := config.Load() 210 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 207 211 if err != nil { 208 212 return fmt.Errorf("config: %w", err) 209 213 } ··· 259 263 return cmd 260 264 } 261 265 262 - func newReindexCmd() *cobra.Command { 266 + func newReindexCmd(local *bool) *cobra.Command { 263 267 return &cobra.Command{ 264 268 Use: "reindex", 265 269 Short: "Re-normalize and upsert all documents", 266 270 RunE: func(cmd *cobra.Command, args []string) error { 267 - cfg, err := config.Load() 271 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 268 272 if err != nil { 269 273 return fmt.Errorf("config: %w", err) 270 274 } ··· 275 279 } 276 280 } 277 281 278 - func newReembedCmd() *cobra.Command { 282 + func newReembedCmd(local *bool) *cobra.Command { 279 283 return &cobra.Command{ 280 284 Use: "reembed", 281 285 Short: "Re-generate all embeddings", 282 286 RunE: func(cmd *cobra.Command, args []string) error { 283 - cfg, err := config.Load() 287 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 284 288 if err != nil { 285 289 return fmt.Errorf("config: %w", err) 286 290 } ··· 291 295 } 292 296 } 293 297 294 - func newHealthcheckCmd() *cobra.Command { 298 + func newHealthcheckCmd(local *bool) *cobra.Command { 295 299 return &cobra.Command{ 296 300 Use: "healthcheck", 297 301 Short: "One-shot health probe", 298 302 RunE: func(cmd *cobra.Command, args []string) error { 299 - cfg, err := config.Load() 303 + cfg, err := config.Load(config.LoadOptions{Local: *local}) 300 304 if err != nil { 301 305 return fmt.Errorf("config: %w", err) 302 306 }