Detailed changes
@@ -13,12 +13,10 @@ import (
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/csync"
- "github.com/charmbracelet/crush/internal/env"
"github.com/tidwall/sjson"
)
const (
- appName = "crush"
defaultDataDirectory = ".crush"
)
@@ -207,7 +205,7 @@ func (m MCPConfig) ResolvedEnv() []string {
}
func (m MCPConfig) ResolvedHeaders() map[string]string {
- resolver := NewShellVariableResolver(env.New())
+ resolver := NewShellVariableResolver(os.Environ())
for e, v := range m.Headers {
var err error
m.Headers[e], err = resolver.ResolveValue(v)
@@ -563,7 +561,7 @@ func (c *ProviderConfig) TestConnection(resolver VariableResolver) error {
}
func resolveEnvs(envs map[string]string) []string {
- resolver := NewShellVariableResolver(env.New())
+ resolver := NewShellVariableResolver(os.Environ())
for e, v := range envs {
var err error
envs[e], err = resolver.ResolveValue(v)
@@ -15,12 +15,15 @@ import (
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/csync"
- "github.com/charmbracelet/crush/internal/env"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/home"
+ "github.com/charmbracelet/crush/internal/version"
+ uv "github.com/charmbracelet/ultraviolet"
powernapConfig "github.com/charmbracelet/x/powernap/pkg/config"
)
+type environ = uv.Environ
+
const defaultCatwalkURL = "https://catwalk.charm.sh"
// LoadReader config via io.Reader.
@@ -62,11 +65,11 @@ func Load(workingDir, dataDir string, debug bool) (*Config, error) {
}
cfg.knownProviders = providers
- env := env.New()
+ envs := os.Environ()
// Configure providers
- valueResolver := NewShellVariableResolver(env)
+ valueResolver := NewShellVariableResolver(envs)
cfg.resolver = valueResolver
- if err := cfg.configureProviders(env, valueResolver, cfg.knownProviders); err != nil {
+ if err := cfg.configureProviders(envs, valueResolver, cfg.knownProviders); err != nil {
return nil, fmt.Errorf("failed to configure providers: %w", err)
}
@@ -110,7 +113,7 @@ func PushPopCrushEnv() func() {
return restore
}
-func (c *Config) configureProviders(env env.Env, resolver VariableResolver, knownProviders []catwalk.Provider) error {
+func (c *Config) configureProviders(env environ, resolver VariableResolver, knownProviders []catwalk.Provider) error {
knownProviderNames := make(map[string]bool)
restore := PushPopCrushEnv()
defer restore()
@@ -185,8 +188,8 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
}
continue
}
- prepared.ExtraParams["project"] = env.Get("VERTEXAI_PROJECT")
- prepared.ExtraParams["location"] = env.Get("VERTEXAI_LOCATION")
+ prepared.ExtraParams["project"] = env.Getenv("VERTEXAI_PROJECT")
+ prepared.ExtraParams["location"] = env.Getenv("VERTEXAI_LOCATION")
case catwalk.InferenceProviderAzure:
endpoint, err := resolver.ResolveValue(p.APIEndpoint)
if err != nil || endpoint == "" {
@@ -197,7 +200,7 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
continue
}
prepared.BaseURL = endpoint
- prepared.ExtraParams["apiVersion"] = env.Get("AZURE_OPENAI_API_VERSION")
+ prepared.ExtraParams["apiVersion"] = env.Getenv("AZURE_OPENAI_API_VERSION")
case catwalk.InferenceProviderBedrock:
if !hasAWSCredentials(env) {
if configExists {
@@ -206,9 +209,9 @@ func (c *Config) configureProviders(env env.Env, resolver VariableResolver, know
}
continue
}
- prepared.ExtraParams["region"] = env.Get("AWS_REGION")
+ prepared.ExtraParams["region"] = env.Getenv("AWS_REGION")
if prepared.ExtraParams["region"] == "" {
- prepared.ExtraParams["region"] = env.Get("AWS_DEFAULT_REGION")
+ prepared.ExtraParams["region"] = env.Getenv("AWS_DEFAULT_REGION")
}
for _, model := range p.Models {
if !strings.HasPrefix(model.ID, "anthropic.") {
@@ -521,7 +524,7 @@ func lookupConfigs(cwd string) []string {
return configPaths
}
- configNames := []string{appName + ".json", "." + appName + ".json"}
+ configNames := []string{version.AppName + ".json", "." + version.AppName + ".json"}
foundConfigs, err := fsext.Lookup(cwd, configNames...)
if err != nil {
@@ -567,27 +570,27 @@ func loadFromReaders(readers []io.Reader) (*Config, error) {
return LoadReader(merged)
}
-func hasVertexCredentials(env env.Env) bool {
- hasProject := env.Get("VERTEXAI_PROJECT") != ""
- hasLocation := env.Get("VERTEXAI_LOCATION") != ""
+func hasVertexCredentials(env environ) bool {
+ hasProject := env.Getenv("VERTEXAI_PROJECT") != ""
+ hasLocation := env.Getenv("VERTEXAI_LOCATION") != ""
return hasProject && hasLocation
}
-func hasAWSCredentials(env env.Env) bool {
- if env.Get("AWS_ACCESS_KEY_ID") != "" && env.Get("AWS_SECRET_ACCESS_KEY") != "" {
+func hasAWSCredentials(env environ) bool {
+ if env.Getenv("AWS_ACCESS_KEY_ID") != "" && env.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
return true
}
- if env.Get("AWS_PROFILE") != "" || env.Get("AWS_DEFAULT_PROFILE") != "" {
+ if env.Getenv("AWS_PROFILE") != "" || env.Getenv("AWS_DEFAULT_PROFILE") != "" {
return true
}
- if env.Get("AWS_REGION") != "" || env.Get("AWS_DEFAULT_REGION") != "" {
+ if env.Getenv("AWS_REGION") != "" || env.Getenv("AWS_DEFAULT_REGION") != "" {
return true
}
- if env.Get("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
- env.Get("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
+ if env.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" ||
+ env.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" {
return true
}
return false
@@ -596,7 +599,7 @@ func hasAWSCredentials(env env.Env) bool {
func globalConfig() string {
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
if xdgConfigHome != "" {
- return filepath.Join(xdgConfigHome, appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(xdgConfigHome, version.AppName, fmt.Sprintf("%s.json", version.AppName))
}
// return the path to the main config directory
@@ -607,10 +610,10 @@ func globalConfig() string {
if localAppData == "" {
localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
}
- return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(localAppData, version.AppName, fmt.Sprintf("%s.json", version.AppName))
}
- return filepath.Join(home.Dir(), ".config", appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(home.Dir(), ".config", version.AppName, fmt.Sprintf("%s.json", version.AppName))
}
// GlobalConfigData returns the path to the main data directory for the application.
@@ -618,7 +621,7 @@ func globalConfig() string {
func GlobalConfigData() string {
xdgDataHome := os.Getenv("XDG_DATA_HOME")
if xdgDataHome != "" {
- return filepath.Join(xdgDataHome, appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(xdgDataHome, version.AppName, fmt.Sprintf("%s.json", version.AppName))
}
// return the path to the main data directory
@@ -629,17 +632,17 @@ func GlobalConfigData() string {
if localAppData == "" {
localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
}
- return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(localAppData, version.AppName, fmt.Sprintf("%s.json", version.AppName))
}
- return filepath.Join(home.Dir(), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(home.Dir(), ".local", "share", version.AppName, fmt.Sprintf("%s.json", version.AppName))
}
// GlobalCacheDir returns the path to the main cache directory for the application.
func GlobalCacheDir() string {
xdgCacheHome := os.Getenv("XDG_CACHE_HOME")
if xdgCacheHome != "" {
- return filepath.Join(xdgCacheHome, appName)
+ return filepath.Join(xdgCacheHome, version.AppName)
}
// return the path to the main cache directory
@@ -650,8 +653,8 @@ func GlobalCacheDir() string {
if localAppData == "" {
localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
}
- return filepath.Join(localAppData, appName, "Cache")
+ return filepath.Join(localAppData, version.AppName, "Cache")
}
- return filepath.Join(home.Dir(), ".cache", appName)
+ return filepath.Join(home.Dir(), ".cache", version.AppName)
}
@@ -10,7 +10,6 @@ import (
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/csync"
- "github.com/charmbracelet/crush/internal/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,8 +22,8 @@ func TestMain(m *testing.M) {
}
func TestConfig_LoadFromReaders(t *testing.T) {
- data1 := strings.NewReader(`{"providers": {"openai": {"api_key": "key1", "base_url": "https://api.openai.com/v1"}}}`)
- data2 := strings.NewReader(`{"providers": {"openai": {"api_key": "key2", "base_url": "https://api.openai.com/v2"}}}`)
+ data1 := strings.NewReader(`{"providers": {"openai": {"api_key=key1", "base_url": "https://api.openai.com/v1"}}}`)
+ data2 := strings.NewReader(`{"providers": {"openai": {"api_key=key2", "base_url": "https://api.openai.com/v2"}}}`)
data3 := strings.NewReader(`{"providers": {"openai": {}}}`)
loadedConfig, err := loadFromReaders([]io.Reader{data1, data2, data3})
@@ -70,8 +69,8 @@ func TestConfig_configureProviders(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "OPENAI_API_KEY": "test-key",
+ env := environ([]string{
+ "OPENAI_API_KEY=test-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -113,8 +112,8 @@ func TestConfig_configureProvidersWithOverride(t *testing.T) {
})
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "OPENAI_API_KEY": "test-key",
+ env := environ([]string{
+ "OPENAI_API_KEY=test-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -155,8 +154,8 @@ func TestConfig_configureProvidersWithNewProvider(t *testing.T) {
}),
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "OPENAI_API_KEY": "test-key",
+ env := environ([]string{
+ "OPENAI_API_KEY=test-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -190,9 +189,9 @@ func TestConfig_configureProvidersBedrockWithCredentials(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "AWS_ACCESS_KEY_ID": "test-key-id",
- "AWS_SECRET_ACCESS_KEY": "test-secret-key",
+ env := environ([]string{
+ "AWS_ACCESS_KEY_ID=test-key-id",
+ "AWS_SECRET_ACCESS_KEY=test-secret-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -219,7 +218,7 @@ func TestConfig_configureProvidersBedrockWithoutCredentials(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -241,9 +240,9 @@ func TestConfig_configureProvidersBedrockWithoutUnsupportedModel(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "AWS_ACCESS_KEY_ID": "test-key-id",
- "AWS_SECRET_ACCESS_KEY": "test-secret-key",
+ env := environ([]string{
+ "AWS_ACCESS_KEY_ID=test-key-id",
+ "AWS_SECRET_ACCESS_KEY=test-secret-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -264,9 +263,9 @@ func TestConfig_configureProvidersVertexAIWithCredentials(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "VERTEXAI_PROJECT": "test-project",
- "VERTEXAI_LOCATION": "us-central1",
+ env := environ([]string{
+ "VERTEXAI_PROJECT=test-project",
+ "VERTEXAI_LOCATION=us-central1",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -295,10 +294,10 @@ func TestConfig_configureProvidersVertexAIWithoutCredentials(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "GOOGLE_GENAI_USE_VERTEXAI": "false",
- "GOOGLE_CLOUD_PROJECT": "test-project",
- "GOOGLE_CLOUD_LOCATION": "us-central1",
+ env := environ([]string{
+ "GOOGLE_GENAI_USE_VERTEXAI=false",
+ "GOOGLE_CLOUD_PROJECT=test-project",
+ "GOOGLE_CLOUD_LOCATION=us-central1",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -321,9 +320,9 @@ func TestConfig_configureProvidersVertexAIMissingProject(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "GOOGLE_GENAI_USE_VERTEXAI": "true",
- "GOOGLE_CLOUD_LOCATION": "us-central1",
+ env := environ([]string{
+ "GOOGLE_GENAI_USE_VERTEXAI=true",
+ "GOOGLE_CLOUD_LOCATION=us-central1",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -346,8 +345,8 @@ func TestConfig_configureProvidersSetProviderID(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "OPENAI_API_KEY": "test-key",
+ env := environ([]string{
+ "OPENAI_API_KEY=test-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -536,8 +535,8 @@ func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "OPENAI_API_KEY": "test-key",
+ env := environ([]string{
+ "OPENAI_API_KEY=test-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -566,7 +565,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -589,7 +588,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -611,7 +610,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -636,7 +635,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -661,7 +660,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -689,7 +688,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -719,7 +718,7 @@ func TestConfig_configureProvidersCustomProviderValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, []catwalk.Provider{})
require.NoError(t, err)
@@ -752,8 +751,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "GOOGLE_GENAI_USE_VERTEXAI": "false",
+ env := environ([]string{
+ "GOOGLE_GENAI_USE_VERTEXAI=false",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -785,7 +784,7 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -816,7 +815,7 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -847,8 +846,8 @@ func TestConfig_configureProvidersEnhancedCredentialValidation(t *testing.T) {
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{
- "OPENAI_API_KEY": "test-key",
+ env := environ([]string{
+ "OPENAI_API_KEY=test-key",
})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
@@ -883,7 +882,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -919,7 +918,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -949,7 +948,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
cfg := &Config{}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -992,7 +991,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
}),
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -1036,7 +1035,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
}),
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -1078,7 +1077,7 @@ func TestConfig_defaultModelSelection(t *testing.T) {
}),
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -1126,7 +1125,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
},
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -1188,7 +1187,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
},
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -1233,7 +1232,7 @@ func TestConfig_configureSelectedModels(t *testing.T) {
},
}
cfg.setDefaults("/tmp", "")
- env := env.NewFromMap(map[string]string{})
+ env := environ([]string{})
resolver := NewEnvironmentVariableResolver(env)
err := cfg.configureProviders(env, resolver, knownProviders)
require.NoError(t, err)
@@ -9,29 +9,23 @@ import (
"path/filepath"
"runtime"
"strings"
- "sync"
"time"
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/catwalk/pkg/embedded"
"github.com/charmbracelet/crush/internal/home"
+ "github.com/charmbracelet/crush/internal/version"
)
type ProviderClient interface {
GetProviders() ([]catwalk.Provider, error)
}
-var (
- providerOnce sync.Once
- providerList []catwalk.Provider
- providerErr error
-)
-
// file to cache provider data
func providerCacheFileData() string {
xdgDataHome := os.Getenv("XDG_DATA_HOME")
if xdgDataHome != "" {
- return filepath.Join(xdgDataHome, appName, "providers.json")
+ return filepath.Join(xdgDataHome, version.AppName, "providers.json")
}
// return the path to the main data directory
@@ -42,10 +36,10 @@ func providerCacheFileData() string {
if localAppData == "" {
localAppData = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
}
- return filepath.Join(localAppData, appName, "providers.json")
+ return filepath.Join(localAppData, version.AppName, "providers.json")
}
- return filepath.Join(home.Dir(), ".local", "share", appName, "providers.json")
+ return filepath.Join(home.Dir(), ".local", "share", version.AppName, "providers.json")
}
func saveProvidersInCache(path string, providers []catwalk.Provider) error {
@@ -114,15 +108,10 @@ func UpdateProviders(pathOrUrl string) error {
}
func Providers(cfg *Config) ([]catwalk.Provider, error) {
- providerOnce.Do(func() {
- catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
- client := catwalk.NewWithURL(catwalkURL)
- path := providerCacheFileData()
-
- autoUpdateDisabled := cfg.Options.DisableProviderAutoUpdate
- providerList, providerErr = loadProviders(autoUpdateDisabled, client, path)
- })
- return providerList, providerErr
+ catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
+ client := catwalk.NewWithURL(catwalkURL)
+ path := providerCacheFileData()
+ return loadProviders(cfg.Options.DisableProviderAutoUpdate, client, path)
}
func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) ([]catwalk.Provider, error) {
@@ -6,7 +6,6 @@ import (
"strings"
"time"
- "github.com/charmbracelet/crush/internal/env"
"github.com/charmbracelet/crush/internal/shell"
)
@@ -20,15 +19,15 @@ type Shell interface {
type shellVariableResolver struct {
shell Shell
- env env.Env
+ env []string
}
-func NewShellVariableResolver(env env.Env) VariableResolver {
+func NewShellVariableResolver(env []string) VariableResolver {
return &shellVariableResolver{
env: env,
shell: shell.NewShell(
&shell.Options{
- Env: env.Env(),
+ Env: env,
},
),
}
@@ -38,6 +37,7 @@ func NewShellVariableResolver(env env.Env) VariableResolver {
// it will resolve shell-like variable substitution anywhere in the string, including:
// - $(command) for command substitution
// - $VAR or ${VAR} for environment variables
+// TODO: can we replace this with [os.Expand](https://pkg.go.dev/os#Expand) somehow?
func (r *shellVariableResolver) ResolveValue(value string) (string, error) {
// Special case: lone $ is an error (backward compatibility)
if value == "$" {
@@ -139,7 +139,7 @@ func (r *shellVariableResolver) ResolveValue(value string) (string, error) {
varName = result[start+1 : end]
}
- envValue := r.env.Get(varName)
+ envValue := environ(r.env).Getenv(varName)
if envValue == "" {
return "", fmt.Errorf("environment variable %q not set", varName)
}
@@ -152,10 +152,10 @@ func (r *shellVariableResolver) ResolveValue(value string) (string, error) {
}
type environmentVariableResolver struct {
- env env.Env
+ env []string
}
-func NewEnvironmentVariableResolver(env env.Env) VariableResolver {
+func NewEnvironmentVariableResolver(env []string) VariableResolver {
return &environmentVariableResolver{
env: env,
}
@@ -168,7 +168,7 @@ func (r *environmentVariableResolver) ResolveValue(value string) (string, error)
}
varName := strings.TrimPrefix(value, "$")
- resolvedValue := r.env.Get(varName)
+ resolvedValue := environ(r.env).Getenv(varName)
if resolvedValue == "" {
return "", fmt.Errorf("environment variable %q not set", varName)
}
@@ -5,7 +5,6 @@ import (
"errors"
"testing"
- "github.com/charmbracelet/crush/internal/env"
"github.com/stretchr/testify/require"
)
@@ -25,7 +24,7 @@ func TestShellVariableResolver_ResolveValue(t *testing.T) {
tests := []struct {
name string
value string
- envVars map[string]string
+ envVars []string
shellFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
expected string
expectError bool
@@ -38,13 +37,13 @@ func TestShellVariableResolver_ResolveValue(t *testing.T) {
{
name: "environment variable resolution",
value: "$HOME",
- envVars: map[string]string{"HOME": "/home/user"},
+ envVars: []string{"HOME=/home/user"},
expected: "/home/user",
},
{
name: "missing environment variable returns error",
value: "$MISSING_VAR",
- envVars: map[string]string{},
+ envVars: []string{},
expectError: true,
},
@@ -76,7 +75,7 @@ func TestShellVariableResolver_ResolveValue(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- testEnv := env.NewFromMap(tt.envVars)
+ testEnv := environ(tt.envVars)
resolver := &shellVariableResolver{
shell: &mockShell{execFunc: tt.shellFunc},
env: testEnv,
@@ -98,7 +97,7 @@ func TestShellVariableResolver_EnhancedResolveValue(t *testing.T) {
tests := []struct {
name string
value string
- envVars map[string]string
+ envVars []string
shellFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
expected string
expectError bool
@@ -117,21 +116,21 @@ func TestShellVariableResolver_EnhancedResolveValue(t *testing.T) {
{
name: "environment variable within string",
value: "Bearer $TOKEN",
- envVars: map[string]string{"TOKEN": "sk-ant-123"},
+ envVars: []string{"TOKEN=sk-ant-123"},
expected: "Bearer sk-ant-123",
},
{
name: "environment variable with braces within string",
value: "Bearer ${TOKEN}",
- envVars: map[string]string{"TOKEN": "sk-ant-456"},
+ envVars: []string{"TOKEN=sk-ant-456"},
expected: "Bearer sk-ant-456",
},
{
name: "mixed command and environment substitution",
value: "$USER-$(date +%Y)-$HOST",
- envVars: map[string]string{
- "USER": "testuser",
- "HOST": "localhost",
+ envVars: []string{
+ "USER=testuser",
+ "HOST=localhost",
},
shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
if command == "date +%Y" {
@@ -179,7 +178,7 @@ func TestShellVariableResolver_EnhancedResolveValue(t *testing.T) {
{
name: "empty environment variable substitution",
value: "Bearer $EMPTY_VAR",
- envVars: map[string]string{},
+ envVars: []string{},
expectError: true,
},
{
@@ -214,7 +213,7 @@ func TestShellVariableResolver_EnhancedResolveValue(t *testing.T) {
{
name: "environment variable with underscores and numbers",
value: "Bearer $API_KEY_V2",
- envVars: map[string]string{"API_KEY_V2": "sk-test-123"},
+ envVars: []string{"API_KEY_V2=sk-test-123"},
expected: "Bearer sk-test-123",
},
{
@@ -241,7 +240,7 @@ func TestShellVariableResolver_EnhancedResolveValue(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- testEnv := env.NewFromMap(tt.envVars)
+ testEnv := environ(tt.envVars)
resolver := &shellVariableResolver{
shell: &mockShell{execFunc: tt.shellFunc},
env: testEnv,
@@ -263,7 +262,7 @@ func TestEnvironmentVariableResolver_ResolveValue(t *testing.T) {
tests := []struct {
name string
value string
- envVars map[string]string
+ envVars []string
expected string
expectError bool
}{
@@ -275,32 +274,32 @@ func TestEnvironmentVariableResolver_ResolveValue(t *testing.T) {
{
name: "environment variable resolution",
value: "$HOME",
- envVars: map[string]string{"HOME": "/home/user"},
+ envVars: []string{"HOME=/home/user"},
expected: "/home/user",
},
{
name: "environment variable with complex value",
value: "$PATH",
- envVars: map[string]string{"PATH": "/usr/bin:/bin:/usr/local/bin"},
+ envVars: []string{"PATH=/usr/bin:/bin:/usr/local/bin"},
expected: "/usr/bin:/bin:/usr/local/bin",
},
{
name: "missing environment variable returns error",
value: "$MISSING_VAR",
- envVars: map[string]string{},
+ envVars: []string{},
expectError: true,
},
{
name: "empty environment variable returns error",
value: "$EMPTY_VAR",
- envVars: map[string]string{"EMPTY_VAR": ""},
+ envVars: []string{"EMPTY_VAR="},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- testEnv := env.NewFromMap(tt.envVars)
+ testEnv := environ(tt.envVars)
resolver := NewEnvironmentVariableResolver(testEnv)
result, err := resolver.ResolveValue(tt.value)
@@ -316,7 +315,7 @@ func TestEnvironmentVariableResolver_ResolveValue(t *testing.T) {
}
func TestNewShellVariableResolver(t *testing.T) {
- testEnv := env.NewFromMap(map[string]string{"TEST": "value"})
+ testEnv := environ([]string{"TEST=value"})
resolver := NewShellVariableResolver(testEnv)
require.NotNil(t, resolver)
@@ -324,7 +323,7 @@ func TestNewShellVariableResolver(t *testing.T) {
}
func TestNewEnvironmentVariableResolver(t *testing.T) {
- testEnv := env.NewFromMap(map[string]string{"TEST": "value"})
+ testEnv := environ([]string{"TEST=value"})
resolver := NewEnvironmentVariableResolver(testEnv)
require.NotNil(t, resolver)
@@ -1,58 +0,0 @@
-package env
-
-import "os"
-
-type Env interface {
- Get(key string) string
- Env() []string
-}
-
-type osEnv struct{}
-
-// Get implements Env.
-func (o *osEnv) Get(key string) string {
- return os.Getenv(key)
-}
-
-func (o *osEnv) Env() []string {
- env := os.Environ()
- if len(env) == 0 {
- return nil
- }
- return env
-}
-
-func New() Env {
- return &osEnv{}
-}
-
-type mapEnv struct {
- m map[string]string
-}
-
-// Get implements Env.
-func (m *mapEnv) Get(key string) string {
- if value, ok := m.m[key]; ok {
- return value
- }
- return ""
-}
-
-// Env implements Env.
-func (m *mapEnv) Env() []string {
- if len(m.m) == 0 {
- return nil
- }
- env := make([]string, 0, len(m.m))
- for k, v := range m.m {
- env = append(env, k+"="+v)
- }
- return env
-}
-
-func NewFromMap(m map[string]string) Env {
- if m == nil {
- m = make(map[string]string)
- }
- return &mapEnv{m: m}
-}
@@ -1,140 +0,0 @@
-package env
-
-import (
- "strings"
- "testing"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestOsEnv_Get(t *testing.T) {
- env := New()
-
- // Test getting an existing environment variable
- t.Setenv("TEST_VAR", "test_value")
-
- value := env.Get("TEST_VAR")
- require.Equal(t, "test_value", value)
-
- // Test getting a non-existent environment variable
- value = env.Get("NON_EXISTENT_VAR")
- require.Equal(t, "", value)
-}
-
-func TestOsEnv_Env(t *testing.T) {
- env := New()
-
- envVars := env.Env()
-
- // Environment should not be empty in normal circumstances
- require.NotNil(t, envVars)
- require.Greater(t, len(envVars), 0)
-
- // Each environment variable should be in key=value format
- for _, envVar := range envVars {
- require.Contains(t, envVar, "=")
- }
-}
-
-func TestNewFromMap(t *testing.T) {
- testMap := map[string]string{
- "KEY1": "value1",
- "KEY2": "value2",
- }
-
- env := NewFromMap(testMap)
- require.NotNil(t, env)
- require.IsType(t, &mapEnv{}, env)
-}
-
-func TestMapEnv_Get(t *testing.T) {
- testMap := map[string]string{
- "KEY1": "value1",
- "KEY2": "value2",
- }
-
- env := NewFromMap(testMap)
-
- // Test getting existing keys
- require.Equal(t, "value1", env.Get("KEY1"))
- require.Equal(t, "value2", env.Get("KEY2"))
-
- // Test getting non-existent key
- require.Equal(t, "", env.Get("NON_EXISTENT"))
-}
-
-func TestMapEnv_Env(t *testing.T) {
- t.Run("with values", func(t *testing.T) {
- testMap := map[string]string{
- "KEY1": "value1",
- "KEY2": "value2",
- }
-
- env := NewFromMap(testMap)
- envVars := env.Env()
-
- require.Len(t, envVars, 2)
-
- // Convert to map for easier testing (order is not guaranteed)
- envMap := make(map[string]string)
- for _, envVar := range envVars {
- parts := strings.SplitN(envVar, "=", 2)
- require.Len(t, parts, 2)
- envMap[parts[0]] = parts[1]
- }
-
- require.Equal(t, "value1", envMap["KEY1"])
- require.Equal(t, "value2", envMap["KEY2"])
- })
-
- t.Run("empty map", func(t *testing.T) {
- env := NewFromMap(map[string]string{})
- envVars := env.Env()
- require.Nil(t, envVars)
- })
-
- t.Run("nil map", func(t *testing.T) {
- env := NewFromMap(nil)
- envVars := env.Env()
- require.Nil(t, envVars)
- })
-}
-
-func TestMapEnv_GetEmptyValue(t *testing.T) {
- testMap := map[string]string{
- "EMPTY_KEY": "",
- "NORMAL_KEY": "value",
- }
-
- env := NewFromMap(testMap)
-
- // Test that empty values are returned correctly
- require.Equal(t, "", env.Get("EMPTY_KEY"))
- require.Equal(t, "value", env.Get("NORMAL_KEY"))
-}
-
-func TestMapEnv_EnvFormat(t *testing.T) {
- testMap := map[string]string{
- "KEY_WITH_EQUALS": "value=with=equals",
- "KEY_WITH_SPACES": "value with spaces",
- }
-
- env := NewFromMap(testMap)
- envVars := env.Env()
-
- require.Len(t, envVars, 2)
-
- // Check that the format is correct even with special characters
- found := make(map[string]bool)
- for _, envVar := range envVars {
- if envVar == "KEY_WITH_EQUALS=value=with=equals" {
- found["equals"] = true
- }
- if envVar == "KEY_WITH_SPACES=value with spaces" {
- found["spaces"] = true
- }
- }
-
- require.True(t, found["equals"], "Should handle values with equals signs")
- require.True(t, found["spaces"], "Should handle values with spaces")
-}
@@ -8,7 +8,6 @@ import (
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/csync"
- "github.com/charmbracelet/crush/internal/env"
"github.com/charmbracelet/crush/internal/home"
)
@@ -49,7 +48,7 @@ func expandPath(path string) string {
// Handle environment variable expansion using the same pattern as config
if strings.HasPrefix(path, "$") {
- resolver := config.NewEnvironmentVariableResolver(env.New())
+ resolver := config.NewEnvironmentVariableResolver(os.Environ())
if expanded, err := resolver.ResolveValue(path); err == nil {
path = expanded
}
@@ -5,7 +5,6 @@ import (
"testing"
"github.com/charmbracelet/crush/internal/config"
- "github.com/charmbracelet/crush/internal/env"
)
func TestClient(t *testing.T) {
@@ -22,9 +21,9 @@ func TestClient(t *testing.T) {
// Test creating a powernap client - this will likely fail with echo
// but we can still test the basic structure
- client, err := New(ctx, &cfg, "test", lspCfg, config.NewEnvironmentVariableResolver(env.NewFromMap(map[string]string{
- "THE_CMD": "echo",
- })))
+ client, err := New(ctx, &cfg, "test", lspCfg, config.NewEnvironmentVariableResolver([]string{
+ "THE_CMD=echo",
+ }))
if err != nil {
// Expected to fail with echo command, skip the rest
t.Skipf("Powernap client creation failed as expected with dummy command: %v", err)
@@ -2,6 +2,8 @@ package version
import "runtime/debug"
+const AppName = "crush"
+
// Build-time parameters set via -ldflags
var (