Skip to content

Commit 8493c03

Browse files
committed
refactor: hardcode per-environment defaults in CLI config
TERO_ENV now drives endpoint and WorkOS client ID defaults for local, dev, and prd. Env vars override but are no longer required. - Add environments map with defaults for local/dev/prd - Add Env field to CLIConfig, simplify Environment() - Remove redundant endpoint and WorkOS vars from .envrc - Add cli_test.go covering env resolution, overrides, fallback
1 parent eefa102 commit 8493c03

3 files changed

Lines changed: 218 additions & 38 deletions

File tree

‎.envrc‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,17 @@
33
# Ensure hermit is loaded by the editor
44
eval "$("$PWD/bin/hermit" env -)"
55

6-
# Environment: local for development, dev/prd via doppler
6+
# Environment: local (default), dev, prd
7+
# TERO_ENV selects per-environment defaults (endpoints, WorkOS client ID).
8+
# Override with TERO_ENV=dev or TERO_ENV=prd. See internal/config/cli.go.
79
export TERO_ENV="local"
810

9-
# Tero CLI configuration - Local control plane (default for development)
10-
export TERO_API_ENDPOINT="http://localhost:8081"
11-
export TERO_CHAT_ENDPOINT="http://localhost:8083"
12-
export TERO_POWERSYNC_ENDPOINT="http://localhost:8084"
1311
# Control plane database (for code generation only - fetches column comments)
1412
export TERO_CONTROL_PLANE_DATABASE_URL="postgres://postgres:postgres@localhost:5432/control_plane_dev?sslmode=disable"
1513
# PowerSync API token for schema generation (local dev uses dev-cli-token)
1614
export POWERSYNC_API_TOKEN="${POWERSYNC_API_TOKEN:-dev-cli-token}"
1715

18-
# WorkOS client ID (same for local dev)
19-
export WORKOS_CLIENT_ID="client_01JQCC2CJMTB8AY2JRMZXFY9R1"
20-
21-
# Staging environment (uncomment to use instead of local)
22-
# export TERO_API_ENDPOINT="https://api.usetero.dev/graphql"
23-
# export TERO_POWERSYNC_ENDPOINT="https://powersync.usetero.dev"
24-
2516
# Development: Skip to app mode after onboarding complete
26-
# Set to "true" to automatically transition from onboarding complete screen to app
2717
export TERO_SKIP_TO_APP="true"
2818

2919
# Development: Enable debug logging (logs written to ~/.tero/environments/{env}/tero.log)

‎internal/config/cli.go‎

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,45 @@
11
package config
22

33
import (
4-
"net/url"
54
"os"
65
)
76

8-
const (
9-
productionEndpoint = "https://api.usetero.com"
10-
productionPowerSyncEndpoint = "https://powersync.usetero.com"
11-
productionChatEndpoint = "https://chat.usetero.com"
12-
)
7+
// environmentDefaults holds the well-known defaults for a named environment.
8+
type environmentDefaults struct {
9+
APIEndpoint string
10+
PowerSyncEndpoint string
11+
ChatEndpoint string
12+
WorkOSClientID string
13+
}
14+
15+
// environments maps TERO_ENV values to their default configuration.
16+
// Unknown environments fall back to prd defaults.
17+
var environments = map[string]environmentDefaults{
18+
"local": {
19+
APIEndpoint: "http://localhost:8081",
20+
PowerSyncEndpoint: "http://localhost:8084",
21+
ChatEndpoint: "http://localhost:8083",
22+
WorkOSClientID: "client_01JQCC2CJMTB8AY2JRMZXFY9R1",
23+
},
24+
"dev": {
25+
APIEndpoint: "https://api.usetero.dev",
26+
PowerSyncEndpoint: "https://powersync.usetero.dev",
27+
ChatEndpoint: "https://chat.usetero.dev",
28+
WorkOSClientID: "client_01JQCC2CJMTB8AY2JRMZXFY9R1",
29+
},
30+
"prd": {
31+
APIEndpoint: "https://api.usetero.com",
32+
PowerSyncEndpoint: "https://powersync.usetero.com",
33+
ChatEndpoint: "https://chat.usetero.com",
34+
WorkOSClientID: "client_01JQCC2D06JF9ASFA6GRHMFA3N",
35+
},
36+
}
1337

1438
// CLIConfig holds configuration for the Tero CLI.
1539
type CLIConfig struct {
40+
// Env is the environment name (local, dev, prd) from TERO_ENV.
41+
Env string
42+
1643
// APIEndpoint is the Tero control plane GraphQL endpoint
1744
APIEndpoint string
1845

@@ -30,33 +57,31 @@ type CLIConfig struct {
3057
}
3158

3259
// LoadCLIConfig loads CLI configuration from environment variables and defaults.
60+
// Resolution: TERO_ENV selects per-environment defaults, then individual env vars override.
3361
func LoadCLIConfig() *CLIConfig {
62+
env := os.Getenv("TERO_ENV")
63+
if env == "" {
64+
env = "prd"
65+
}
66+
67+
defaults := environments["prd"]
68+
if d, ok := environments[env]; ok {
69+
defaults = d
70+
}
71+
3472
return &CLIConfig{
35-
APIEndpoint: getEnvOrDefault("TERO_API_ENDPOINT", productionEndpoint),
36-
PowerSyncEndpoint: getEnvOrDefault("TERO_POWERSYNC_ENDPOINT", productionPowerSyncEndpoint),
37-
ChatEndpoint: getEnvOrDefault("TERO_CHAT_ENDPOINT", productionChatEndpoint),
38-
WorkOSClientID: getEnvOrDefault("WORKOS_CLIENT_ID", "client_01JQCC2D06JF9ASFA6GRHMFA3N"),
73+
Env: env,
74+
APIEndpoint: getEnvOrDefault("TERO_API_ENDPOINT", defaults.APIEndpoint),
75+
PowerSyncEndpoint: getEnvOrDefault("TERO_POWERSYNC_ENDPOINT", defaults.PowerSyncEndpoint),
76+
ChatEndpoint: getEnvOrDefault("TERO_CHAT_ENDPOINT", defaults.ChatEndpoint),
77+
WorkOSClientID: getEnvOrDefault("WORKOS_CLIENT_ID", defaults.WorkOSClientID),
3978
Debug: os.Getenv("TERO_DEBUG") == "true" || os.Getenv("TERO_DEBUG") == "1",
4079
}
4180
}
4281

4382
// Environment returns the environment name for data isolation.
44-
// Resolution order:
45-
// 1. TERO_ENV env var (explicit: "local", "dev", "prd", or any custom value)
46-
// 2. Production API endpoint → "prd"
47-
// 3. Non-production → derive from URL host (fallback for custom setups)
4883
func (c *CLIConfig) Environment() string {
49-
if env := os.Getenv("TERO_ENV"); env != "" {
50-
return env
51-
}
52-
if c.APIEndpoint == productionEndpoint {
53-
return "prd"
54-
}
55-
u, err := url.Parse(c.APIEndpoint)
56-
if err != nil {
57-
return "prd"
58-
}
59-
return u.Host
84+
return c.Env
6085
}
6186

6287
func getEnvOrDefault(key, defaultValue string) string {

‎internal/config/cli_test.go‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestLoadCLIConfig_DefaultsToPrd(t *testing.T) {
8+
t.Setenv("TERO_ENV", "")
9+
t.Setenv("TERO_API_ENDPOINT", "")
10+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
11+
t.Setenv("TERO_CHAT_ENDPOINT", "")
12+
t.Setenv("WORKOS_CLIENT_ID", "")
13+
t.Setenv("TERO_DEBUG", "")
14+
15+
cfg := LoadCLIConfig()
16+
17+
if cfg.Env != "prd" {
18+
t.Errorf("Env = %q, want %q", cfg.Env, "prd")
19+
}
20+
if cfg.Environment() != "prd" {
21+
t.Errorf("Environment() = %q, want %q", cfg.Environment(), "prd")
22+
}
23+
if cfg.APIEndpoint != "https://api.usetero.com" {
24+
t.Errorf("APIEndpoint = %q, want production", cfg.APIEndpoint)
25+
}
26+
if cfg.PowerSyncEndpoint != "https://powersync.usetero.com" {
27+
t.Errorf("PowerSyncEndpoint = %q, want production", cfg.PowerSyncEndpoint)
28+
}
29+
if cfg.ChatEndpoint != "https://chat.usetero.com" {
30+
t.Errorf("ChatEndpoint = %q, want production", cfg.ChatEndpoint)
31+
}
32+
if cfg.WorkOSClientID != "client_01JQCC2D06JF9ASFA6GRHMFA3N" {
33+
t.Errorf("WorkOSClientID = %q, want production client", cfg.WorkOSClientID)
34+
}
35+
}
36+
37+
func TestLoadCLIConfig_Local(t *testing.T) {
38+
t.Setenv("TERO_ENV", "local")
39+
t.Setenv("TERO_API_ENDPOINT", "")
40+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
41+
t.Setenv("TERO_CHAT_ENDPOINT", "")
42+
t.Setenv("WORKOS_CLIENT_ID", "")
43+
44+
cfg := LoadCLIConfig()
45+
46+
if cfg.Env != "local" {
47+
t.Errorf("Env = %q, want %q", cfg.Env, "local")
48+
}
49+
if cfg.APIEndpoint != "http://localhost:8081" {
50+
t.Errorf("APIEndpoint = %q, want localhost", cfg.APIEndpoint)
51+
}
52+
if cfg.PowerSyncEndpoint != "http://localhost:8084" {
53+
t.Errorf("PowerSyncEndpoint = %q, want localhost", cfg.PowerSyncEndpoint)
54+
}
55+
if cfg.ChatEndpoint != "http://localhost:8083" {
56+
t.Errorf("ChatEndpoint = %q, want localhost", cfg.ChatEndpoint)
57+
}
58+
if cfg.WorkOSClientID != "client_01JQCC2CJMTB8AY2JRMZXFY9R1" {
59+
t.Errorf("WorkOSClientID = %q, want local/dev client", cfg.WorkOSClientID)
60+
}
61+
}
62+
63+
func TestLoadCLIConfig_Dev(t *testing.T) {
64+
t.Setenv("TERO_ENV", "dev")
65+
t.Setenv("TERO_API_ENDPOINT", "")
66+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
67+
t.Setenv("TERO_CHAT_ENDPOINT", "")
68+
t.Setenv("WORKOS_CLIENT_ID", "")
69+
70+
cfg := LoadCLIConfig()
71+
72+
if cfg.Env != "dev" {
73+
t.Errorf("Env = %q, want %q", cfg.Env, "dev")
74+
}
75+
if cfg.APIEndpoint != "https://api.usetero.dev" {
76+
t.Errorf("APIEndpoint = %q, want dev", cfg.APIEndpoint)
77+
}
78+
if cfg.PowerSyncEndpoint != "https://powersync.usetero.dev" {
79+
t.Errorf("PowerSyncEndpoint = %q, want dev", cfg.PowerSyncEndpoint)
80+
}
81+
if cfg.ChatEndpoint != "https://chat.usetero.dev" {
82+
t.Errorf("ChatEndpoint = %q, want dev", cfg.ChatEndpoint)
83+
}
84+
}
85+
86+
func TestLoadCLIConfig_Prd(t *testing.T) {
87+
t.Setenv("TERO_ENV", "prd")
88+
t.Setenv("TERO_API_ENDPOINT", "")
89+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
90+
t.Setenv("TERO_CHAT_ENDPOINT", "")
91+
t.Setenv("WORKOS_CLIENT_ID", "")
92+
93+
cfg := LoadCLIConfig()
94+
95+
if cfg.Env != "prd" {
96+
t.Errorf("Env = %q, want %q", cfg.Env, "prd")
97+
}
98+
if cfg.APIEndpoint != "https://api.usetero.com" {
99+
t.Errorf("APIEndpoint = %q, want production", cfg.APIEndpoint)
100+
}
101+
}
102+
103+
func TestLoadCLIConfig_UnknownEnvFallsToPrd(t *testing.T) {
104+
t.Setenv("TERO_ENV", "staging")
105+
t.Setenv("TERO_API_ENDPOINT", "")
106+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
107+
t.Setenv("TERO_CHAT_ENDPOINT", "")
108+
t.Setenv("WORKOS_CLIENT_ID", "")
109+
110+
cfg := LoadCLIConfig()
111+
112+
if cfg.Env != "staging" {
113+
t.Errorf("Env = %q, want %q", cfg.Env, "staging")
114+
}
115+
if cfg.Environment() != "staging" {
116+
t.Errorf("Environment() = %q, want %q", cfg.Environment(), "staging")
117+
}
118+
if cfg.APIEndpoint != "https://api.usetero.com" {
119+
t.Errorf("APIEndpoint = %q, want production fallback", cfg.APIEndpoint)
120+
}
121+
}
122+
123+
func TestLoadCLIConfig_EnvVarOverridesDefaults(t *testing.T) {
124+
t.Setenv("TERO_ENV", "dev")
125+
t.Setenv("TERO_API_ENDPOINT", "http://custom:9999")
126+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
127+
t.Setenv("TERO_CHAT_ENDPOINT", "")
128+
t.Setenv("WORKOS_CLIENT_ID", "")
129+
130+
cfg := LoadCLIConfig()
131+
132+
if cfg.APIEndpoint != "http://custom:9999" {
133+
t.Errorf("APIEndpoint = %q, want custom override", cfg.APIEndpoint)
134+
}
135+
// Other fields still use dev defaults
136+
if cfg.PowerSyncEndpoint != "https://powersync.usetero.dev" {
137+
t.Errorf("PowerSyncEndpoint = %q, want dev default", cfg.PowerSyncEndpoint)
138+
}
139+
}
140+
141+
func TestLoadCLIConfig_Debug(t *testing.T) {
142+
t.Setenv("TERO_ENV", "prd")
143+
t.Setenv("TERO_API_ENDPOINT", "")
144+
t.Setenv("TERO_POWERSYNC_ENDPOINT", "")
145+
t.Setenv("TERO_CHAT_ENDPOINT", "")
146+
t.Setenv("WORKOS_CLIENT_ID", "")
147+
148+
t.Setenv("TERO_DEBUG", "true")
149+
cfg := LoadCLIConfig()
150+
if !cfg.Debug {
151+
t.Error("Debug should be true when TERO_DEBUG=true")
152+
}
153+
154+
t.Setenv("TERO_DEBUG", "1")
155+
cfg = LoadCLIConfig()
156+
if !cfg.Debug {
157+
t.Error("Debug should be true when TERO_DEBUG=1")
158+
}
159+
160+
t.Setenv("TERO_DEBUG", "")
161+
cfg = LoadCLIConfig()
162+
if cfg.Debug {
163+
t.Error("Debug should be false when TERO_DEBUG is empty")
164+
}
165+
}

0 commit comments

Comments
 (0)