From 905d096ce391545eb1e1352f3fb083f2dda2d5a1 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 11:36:17 +0100 Subject: [PATCH 1/7] Make the user context a hard requirement --- cmd/login/login.go | 3 ++- cmd/login/login_test.go | 44 +++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 19 ++++++++++++++++-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/cmd/login/login.go b/cmd/login/login.go index 5baa5ea8..1c0ce616 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -127,7 +127,8 @@ func (h *handler) execute(ctx context.Context) error { h.spinner.Update("Fetching user context...") if err := h.fetchTenantConfig(ctx, tokenSet); err != nil { - h.log.Debug().Err(err).Msgf("failed to fetch user context — %s not written", tenantctx.ContextFile) + h.spinner.StopAll() + return fmt.Errorf("failed to fetch user context: %w", err) } // Stop spinner before final output diff --git a/cmd/login/login_test.go b/cmd/login/login_test.go index 33bdb2fb..a5abfedb 100644 --- a/cmd/login/login_test.go +++ b/cmd/login/login_test.go @@ -1,6 +1,8 @@ package login import ( + "context" + "encoding/json" "io" "net/http" "net/http/httptest" @@ -16,9 +18,51 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/oauth" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" ) +func TestFetchTenantConfig_GQLError_ReturnsError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "unauthorized"}}, + }) + })) + defer srv.Close() + + tmp := t.TempDir() + t.Setenv("HOME", tmp) + log := zerolog.Nop() + h := &handler{ + log: &log, + environmentSet: &environments.EnvironmentSet{ + EnvName: "STAGING", + GraphQLURL: srv.URL, + }, + } + + tokenSet := &credentials.CreLoginTokenSet{ + AccessToken: "test-access-token", + IDToken: "test-id-token", + ExpiresIn: 3600, + TokenType: "Bearer", + } + + err := h.fetchTenantConfig(context.Background(), tokenSet) + if err == nil { + t.Fatal("expected error when GQL fetch fails") + } + if !strings.Contains(err.Error(), "fetch user context") { + t.Errorf("expected fetch user context error, got: %v", err) + } + + contextPath := filepath.Join(tmp, credentials.ConfigDir, tenantctx.ContextFile) + if _, statErr := os.Stat(contextPath); statErr == nil { + t.Errorf("expected %s not to be written on fetch failure", tenantctx.ContextFile) + } +} + func TestLogin_NonInteractive_ReturnsError(t *testing.T) { // Create a parent command with the global --non-interactive persistent flag, // since in production this flag is defined on the root command. diff --git a/cmd/root.go b/cmd/root.go index aa149602..88191efe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,6 +34,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/telemetry" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" intupdate "github.com/smartcontractkit/cre-cli/internal/update" ) @@ -236,7 +237,14 @@ func newRootCommand() *cobra.Command { spinner.Update("Loading user context...") } if err := runtimeContext.AttachTenantContext(cmd.Context()); err != nil { - ui.Warning("Failed to load user context") + if showSpinner { + spinner.Stop() + } + ui.ErrorWithSuggestions("Failed to load user context", []string{ + "Run `cre login` to fetch your user context", + fmt.Sprintf("Ensure ~/.cre/%s exists and is readable", tenantctx.ContextFile), + }) + return fmt.Errorf("user context required: %w", err) } // Check if organization is ungated for commands that require it @@ -295,7 +303,14 @@ func newRootCommand() *cobra.Command { spinner.Update("Loading user context...") } if err := runtimeContext.AttachTenantContext(cmd.Context()); err != nil { - ui.Warning("Failed to load user context") + if showSpinner { + spinner.Stop() + } + ui.ErrorWithSuggestions("Failed to load user context", []string{ + "Run `cre login` to fetch your user context", + fmt.Sprintf("Ensure ~/.cre/%s exists and is readable", tenantctx.ContextFile), + }) + return fmt.Errorf("user context required: %w", err) } } } From f6e39f5f7592d16d06b8be3c011c9deca0631504 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 12:01:38 +0100 Subject: [PATCH 2/7] e2e tests --- internal/testutil/graphql_mock.go | 63 ++++++++++++++++--- .../multi_command_flows/account_happy_path.go | 13 ++-- .../multi_command_flows/secrets_happy_path.go | 27 ++++---- .../workflow_happy_path_1.go | 14 ++--- .../workflow_happy_path_2.go | 27 ++++---- .../workflow_happy_path_3.go | 40 +++++------- 6 files changed, 103 insertions(+), 81 deletions(-) diff --git a/internal/testutil/graphql_mock.go b/internal/testutil/graphql_mock.go index 9f5ff73f..888f6f7e 100644 --- a/internal/testutil/graphql_mock.go +++ b/internal/testutil/graphql_mock.go @@ -10,8 +10,56 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" ) +// MockGetCreOrganizationInfoGraphQLPayload returns a GraphQL response for getCreOrganizationInfo. +func MockGetCreOrganizationInfoGraphQLPayload() map[string]any { + return map[string]any{ + "data": map[string]any{ + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, + }, + }, + } +} + +// QueryIsGetTenantConfig reports whether q is a getTenantConfig GraphQL operation. +func QueryIsGetTenantConfig(q string) bool { + return strings.Contains(q, "GetTenantConfig") || strings.Contains(q, "getTenantConfig") +} + +// MockGetTenantConfigGraphQLPayload returns a GraphQL response for getTenantConfig +// suitable for E2E tests using the anvil-devnet workflow registry defaults. +func MockGetTenantConfigGraphQLPayload() map[string]any { + return map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "test-tenant-id", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "anvil-devnet", + "label": "anvil-devnet", + "type": "ON_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"OWNER_KEY_SIGNING"}, + }, + { + "id": "private", + "label": "Private (Chainlink-hosted)", + "type": "OFF_CHAIN", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + "forwarders": []any{}, + }, + }, + } +} + // NewGraphQLMockServerGetOrganization starts an httptest.Server that responds to -// getCreOrganizationInfo with a fixed orgId and derivedWorkflowOwners. +// getCreOrganizationInfo and getTenantConfig with fixed test payloads. // It sets EnvVarGraphQLURL so CLI commands use this server. Caller must defer srv.Close(). func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { t.Helper() @@ -24,14 +72,11 @@ func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { _ = json.NewDecoder(r.Body).Decode(&req) w.Header().Set("Content-Type", "application/json") if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(MockGetTenantConfigGraphQLPayload()) return } w.WriteHeader(http.StatusBadRequest) diff --git a/test/multi_command_flows/account_happy_path.go b/test/multi_command_flows/account_happy_path.go index e769d834..c31aba3b 100644 --- a/test/multi_command_flows/account_happy_path.go +++ b/test/multi_command_flows/account_happy_path.go @@ -58,14 +58,11 @@ func RunAccountHappyPath(t *testing.T, tc TestConfig, testEthURL, chainName stri // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } diff --git a/test/multi_command_flows/secrets_happy_path.go b/test/multi_command_flows/secrets_happy_path.go index f37fbfdf..71974060 100644 --- a/test/multi_command_flows/secrets_happy_path.go +++ b/test/multi_command_flows/secrets_happy_path.go @@ -24,6 +24,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil" ) // Hex-encoded tdh2easy.PublicKey blob returned by the gateway @@ -63,14 +64,11 @@ func RunSecretsHappyPath(t *testing.T, tc TestConfig, chainName string) { // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } @@ -258,14 +256,11 @@ func RunSecretsListMsig(t *testing.T, tc TestConfig, chainName string) { // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } diff --git a/test/multi_command_flows/workflow_happy_path_1.go b/test/multi_command_flows/workflow_happy_path_1.go index 4433ac62..236cb652 100644 --- a/test/multi_command_flows/workflow_happy_path_1.go +++ b/test/multi_command_flows/workflow_happy_path_1.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil" ) // TestConfig represents test configuration @@ -62,14 +63,11 @@ func workflowDeployEoaWithMockStorage(t *testing.T, tc TestConfig) (output strin // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } diff --git a/test/multi_command_flows/workflow_happy_path_2.go b/test/multi_command_flows/workflow_happy_path_2.go index 42c52bb8..eed85044 100644 --- a/test/multi_command_flows/workflow_happy_path_2.go +++ b/test/multi_command_flows/workflow_happy_path_2.go @@ -17,6 +17,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil" ) // workflowDeployEoa deploys a workflow via CLI, mocking GraphQL + Origin. @@ -35,14 +36,11 @@ func workflowDeployEoa(t *testing.T, tc TestConfig) string { // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } @@ -156,14 +154,11 @@ func workflowDeployUpdateWithConfig(t *testing.T, tc TestConfig) string { // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } diff --git a/test/multi_command_flows/workflow_happy_path_3.go b/test/multi_command_flows/workflow_happy_path_3.go index 5b890207..5e125e74 100644 --- a/test/multi_command_flows/workflow_happy_path_3.go +++ b/test/multi_command_flows/workflow_happy_path_3.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil" ) // workflowInit runs cre init to initialize a new workflow project from scratch @@ -32,14 +33,11 @@ func workflowInit(t *testing.T, projectRootFlag, projectName, workflowName strin // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } @@ -101,14 +99,11 @@ func workflowDeployUnsigned(t *testing.T, tc TestConfig, projectRootFlag, workfl // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } @@ -220,14 +215,11 @@ func workflowDeployWithConfigAndLinkedKey(t *testing.T, tc TestConfig, projectRo // Handle authentication validation query if strings.Contains(req.Query, "getCreOrganizationInfo") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getCreOrganizationInfo": map[string]any{ - "orgId": "test-org-id", - "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, - }, - }, - }) + _ = json.NewEncoder(w).Encode(testutil.MockGetCreOrganizationInfoGraphQLPayload()) + return + } + if testutil.QueryIsGetTenantConfig(req.Query) { + _ = json.NewEncoder(w).Encode(testutil.MockGetTenantConfigGraphQLPayload()) return } From 25b6b21f9a4ab827480a48ea36f386b0f7dd48e1 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 14:43:55 +0100 Subject: [PATCH 3/7] Extract internal/creconfig for CLI config paths --- cmd/login/login_test.go | 11 +- cmd/logout/logout.go | 15 +-- cmd/logout/logout_test.go | 15 ++- cmd/root.go | 5 +- cmd/secrets/common/browser_flow.go | 2 +- cmd/secrets/common/gateway/resolver.go | 2 +- cmd/templates/add/add.go | 3 +- cmd/templates/remove/remove.go | 3 +- docs/cre_templates_add.md | 2 +- docs/cre_templates_remove.md | 2 +- internal/creconfig/creconfig.go | 63 +++++++++++ internal/creconfig/creconfig_test.go | 103 ++++++++++++++++++ internal/credentials/credentials.go | 14 +-- internal/credentials/credentials_test.go | 5 +- internal/telemetry/collector.go | 9 +- internal/templateconfig/templateconfig.go | 35 ++---- .../templateconfig/templateconfig_test.go | 11 +- internal/templaterepo/cache.go | 13 +-- internal/tenantctx/tenantctx.go | 20 ++-- internal/update/update.go | 20 ++-- .../workflow_private_registry.go | 10 +- 21 files changed, 267 insertions(+), 96 deletions(-) create mode 100644 internal/creconfig/creconfig.go create mode 100644 internal/creconfig/creconfig_test.go diff --git a/cmd/login/login_test.go b/cmd/login/login_test.go index a5abfedb..222e20ec 100644 --- a/cmd/login/login_test.go +++ b/cmd/login/login_test.go @@ -16,6 +16,7 @@ import ( "gopkg.in/yaml.v3" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/oauth" "github.com/smartcontractkit/cre-cli/internal/tenantctx" @@ -57,7 +58,10 @@ func TestFetchTenantConfig_GQLError_ReturnsError(t *testing.T) { t.Errorf("expected fetch user context error, got: %v", err) } - contextPath := filepath.Join(tmp, credentials.ConfigDir, tenantctx.ContextFile) + contextPath, err := creconfig.FilePath(tenantctx.ContextFile) + if err != nil { + t.Fatalf("failed to resolve context path: %v", err) + } if _, statErr := os.Stat(contextPath); statErr == nil { t.Errorf("expected %s not to be written on fetch failure", tenantctx.ContextFile) } @@ -103,7 +107,10 @@ func TestSaveCredentials_WritesYAML(t *testing.T) { t.Fatalf("saveCredentials error: %v", err) } - path := filepath.Join(tmp, credentials.ConfigDir, credentials.ConfigFile) + path, err := creconfig.FilePath(credentials.ConfigFile) + if err != nil { + t.Fatalf("failed to resolve config path: %v", err) + } data, err := os.ReadFile(path) if err != nil { t.Fatalf("cannot read config file: %v", err) diff --git a/cmd/logout/logout.go b/cmd/logout/logout.go index 5411c8bc..d274202f 100644 --- a/cmd/logout/logout.go +++ b/cmd/logout/logout.go @@ -5,13 +5,13 @@ import ( "net/http" "net/url" "os" - "path/filepath" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/tenantctx" @@ -49,11 +49,10 @@ func newHandler(ctx *runtime.Context) *handler { } func (h *handler) execute() error { - home, err := os.UserHomeDir() + credPath, err := creconfig.FilePath(credentials.ConfigFile) if err != nil { - return fmt.Errorf("could not determine home directory: %w", err) + return fmt.Errorf("could not determine config path: %w", err) } - credPath := filepath.Join(home, credentials.ConfigDir, credentials.ConfigFile) // Load credentials directly (logout is excluded from global credential loading) creds, err := credentials.New(h.log) @@ -90,11 +89,13 @@ func (h *handler) execute() error { if err := credentials.SecureRemove(credPath); err != nil { spinner.Stop() - return fmt.Errorf("failed to delete credentials file: %w", err) + return fmt.Errorf("failed to delete credentials file %s: %w", credPath, err) } - contextPath := filepath.Join(home, credentials.ConfigDir, tenantctx.ContextFile) - if err := os.Remove(contextPath); err != nil && !os.IsNotExist(err) { + contextPath, err := creconfig.FilePath(tenantctx.ContextFile) + if err != nil { + h.log.Warn().Err(err).Msgf("failed to resolve %s path", tenantctx.ContextFile) + } else if err := os.Remove(contextPath); err != nil && !os.IsNotExist(err) { h.log.Warn().Err(err).Msgf("failed to delete %s", tenantctx.ContextFile) } diff --git a/cmd/logout/logout_test.go b/cmd/logout/logout_test.go index 187991f2..59899fbb 100644 --- a/cmd/logout/logout_test.go +++ b/cmd/logout/logout_test.go @@ -10,6 +10,7 @@ import ( "gopkg.in/yaml.v3" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/testutil" @@ -17,8 +18,8 @@ import ( func setupCredentialFile(t *testing.T, home string, token string) { t.Helper() - dir := filepath.Join(home, credentials.ConfigDir) - if err := os.MkdirAll(dir, 0o700); err != nil { + dir, err := creconfig.EnsureDir() + if err != nil { t.Fatalf("failed to create config dir: %v", err) } path := filepath.Join(dir, credentials.ConfigFile) @@ -112,7 +113,10 @@ func TestExecute_SuccessRevocationAndRemoval(t *testing.T) { t.Error("expected revocation request, but none received") } - credPath := filepath.Join(tDir, credentials.ConfigDir, credentials.ConfigFile) + credPath, err := creconfig.FilePath(credentials.ConfigFile) + if err != nil { + t.Fatalf("failed to resolve credentials path: %v", err) + } if _, err := os.Stat(credPath); !os.IsNotExist(err) { t.Errorf("expected credentials file to be removed, but it exists") } @@ -152,7 +156,10 @@ func TestExecute_RevocationFails_StillRemovesFile(t *testing.T) { t.Fatalf("expected no error despite revocation failure, got %v", err) } - credPath := filepath.Join(tDir, credentials.ConfigDir, credentials.ConfigFile) + credPath, err := creconfig.FilePath(credentials.ConfigFile) + if err != nil { + t.Fatalf("failed to resolve credentials path: %v", err) + } if _, err := os.Stat(credPath); !os.IsNotExist(err) { t.Errorf("expected credentials file to be removed, but it exists") } diff --git a/cmd/root.go b/cmd/root.go index 88191efe..53e7e3aa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,6 +30,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/context" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/logger" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" @@ -242,7 +243,7 @@ func newRootCommand() *cobra.Command { } ui.ErrorWithSuggestions("Failed to load user context", []string{ "Run `cre login` to fetch your user context", - fmt.Sprintf("Ensure ~/.cre/%s exists and is readable", tenantctx.ContextFile), + fmt.Sprintf("Ensure %s exists and is readable", creconfig.FilePathHint(tenantctx.ContextFile)), }) return fmt.Errorf("user context required: %w", err) } @@ -308,7 +309,7 @@ func newRootCommand() *cobra.Command { } ui.ErrorWithSuggestions("Failed to load user context", []string{ "Run `cre login` to fetch your user context", - fmt.Sprintf("Ensure ~/.cre/%s exists and is readable", tenantctx.ContextFile), + fmt.Sprintf("Ensure %s exists and is readable", creconfig.FilePathHint(tenantctx.ContextFile)), }) return fmt.Errorf("user context required: %w", err) } diff --git a/cmd/secrets/common/browser_flow.go b/cmd/secrets/common/browser_flow.go index 8fe264af..cb626cac 100644 --- a/cmd/secrets/common/browser_flow.go +++ b/cmd/secrets/common/browser_flow.go @@ -61,7 +61,7 @@ func digestHexString(digest [32]byte) string { // executeBrowserUpsert handles secrets create/update when the user signs in with their organization account. // It encrypts the payload, binds a digest, requests a platform authorization URL, completes OAuth in the browser, // exchanges the code for a short-lived vault JWT, and POSTs the same JSON-RPC body to the gateway with Bearer auth. -// Login tokens in ~/.cre/cre.yaml are not modified; that session stays separate from this vault-only token. +// Login tokens in the CLI credentials file are not modified; that session stays separate from this vault-only token. func (h *Handler) executeBrowserUpsert(ctx context.Context, inputs UpsertSecretsInputs, method string) error { if h.Credentials.AuthType == credentials.AuthTypeApiKey { return fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported") diff --git a/cmd/secrets/common/gateway/resolver.go b/cmd/secrets/common/gateway/resolver.go index c57782a4..2463a1b0 100644 --- a/cmd/secrets/common/gateway/resolver.go +++ b/cmd/secrets/common/gateway/resolver.go @@ -9,7 +9,7 @@ import ( ) // ResolveVaultGatewayURL returns the vault gateway URL for secrets operations. -// Precedence: CRE_VAULT_DON_GATEWAY_URL env var, then context.yaml vault_gateway_url, +// Precedence: CRE_VAULT_DON_GATEWAY_URL env var, then the user context file vault_gateway_url, // then the embedded default from EnvironmentSet. func ResolveVaultGatewayURL(tenantCtx *tenantctx.EnvironmentContext, envSet *environments.EnvironmentSet) string { if os.Getenv(environments.EnvVarVaultGatewayURL) != "" && envSet != nil { diff --git a/cmd/templates/add/add.go b/cmd/templates/add/add.go index f531a6c6..b1d09e29 100644 --- a/cmd/templates/add/add.go +++ b/cmd/templates/add/add.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" @@ -20,7 +21,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return &cobra.Command{ Use: "add ...", Short: "Adds a template repository source", - Long: `Adds one or more template repository sources to ~/.cre/template.yaml. These repositories are used by cre init to discover available templates.`, + Long: fmt.Sprintf("Adds one or more template repository sources to %s. These repositories are used by cre init to discover available templates.", creconfig.FileRelPath(templateconfig.TemplateConfigFile)), Args: cobra.MinimumNArgs(1), Example: "cre templates add smartcontractkit/cre-templates@main myorg/my-templates", RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/templates/remove/remove.go b/cmd/templates/remove/remove.go index a8b36787..762298c9 100644 --- a/cmd/templates/remove/remove.go +++ b/cmd/templates/remove/remove.go @@ -6,6 +6,7 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" @@ -20,7 +21,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return &cobra.Command{ Use: "remove ...", Short: "Removes a template repository source", - Long: `Removes one or more template repository sources from ~/.cre/template.yaml. The ref portion is optional and ignored during matching.`, + Long: fmt.Sprintf("Removes one or more template repository sources from %s. The ref portion is optional and ignored during matching.", creconfig.FileRelPath(templateconfig.TemplateConfigFile)), Args: cobra.MinimumNArgs(1), Example: "cre templates remove smartcontractkit/cre-templates myorg/my-templates", RunE: func(cmd *cobra.Command, args []string) error { diff --git a/docs/cre_templates_add.md b/docs/cre_templates_add.md index adaa6303..74933c5d 100644 --- a/docs/cre_templates_add.md +++ b/docs/cre_templates_add.md @@ -4,7 +4,7 @@ Adds a template repository source ### Synopsis -Adds one or more template repository sources to ~/.cre/template.yaml. These repositories are used by cre init to discover available templates. +Adds one or more template repository sources to .cre/template.yaml. These repositories are used by cre init to discover available templates. ``` cre templates add ... [flags] diff --git a/docs/cre_templates_remove.md b/docs/cre_templates_remove.md index 23730215..5f1afa5e 100644 --- a/docs/cre_templates_remove.md +++ b/docs/cre_templates_remove.md @@ -4,7 +4,7 @@ Removes a template repository source ### Synopsis -Removes one or more template repository sources from ~/.cre/template.yaml. The ref portion is optional and ignored during matching. +Removes one or more template repository sources from .cre/template.yaml. The ref portion is optional and ignored during matching. ``` cre templates remove ... [optional flags] diff --git a/internal/creconfig/creconfig.go b/internal/creconfig/creconfig.go new file mode 100644 index 00000000..7ca27316 --- /dev/null +++ b/internal/creconfig/creconfig.go @@ -0,0 +1,63 @@ +package creconfig + +import ( + "fmt" + "os" + "path/filepath" +) + +const Dir = ".cre" + +// DirPath returns the absolute path to the CLI config directory. +func DirPath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("get home dir: %w", err) + } + return filepath.Join(home, Dir), nil +} + +// EnsureDir creates the CLI config directory with 0700 permissions if missing. +func EnsureDir() (string, error) { + dir, err := DirPath() + if err != nil { + return "", err + } + if err := os.MkdirAll(dir, 0o700); err != nil { + return "", fmt.Errorf("create config dir: %w", err) + } + return dir, nil +} + +// FilePath returns the absolute path to a file directly under the CLI config directory. +func FilePath(name string) (string, error) { + dir, err := DirPath() + if err != nil { + return "", err + } + return filepath.Join(dir, name), nil +} + +// FileRelPath returns a home-relative path for static help text and committed docs. +// It uses the host OS path separator (e.g. ".cre/template.yaml" or ".cre\template.yaml"). +func FileRelPath(name string) string { + return filepath.Join(Dir, name) +} + +// FilePathHint returns the absolute config file path for user-facing messages, +// or a home-relative path if the home directory cannot be resolved. +func FilePathHint(name string) string { + if path, err := FilePath(name); err == nil { + return path + } + return FileRelPath(name) +} + +// JoinPath returns an absolute path under the CLI config directory. +func JoinPath(elem ...string) (string, error) { + dir, err := DirPath() + if err != nil { + return "", err + } + return filepath.Join(append([]string{dir}, elem...)...), nil +} diff --git a/internal/creconfig/creconfig_test.go b/internal/creconfig/creconfig_test.go new file mode 100644 index 00000000..5bcf7cc4 --- /dev/null +++ b/internal/creconfig/creconfig_test.go @@ -0,0 +1,103 @@ +package creconfig + +import ( + "os" + "path/filepath" + "testing" +) + +func TestFileRelPath(t *testing.T) { + got := FileRelPath("context.yaml") + want := filepath.Join(Dir, "context.yaml") + if got != want { + t.Fatalf("FileRelPath() = %q, want %q", got, want) + } +} + +func TestDirPath(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + got, err := DirPath() + if err != nil { + t.Fatalf("DirPath() error: %v", err) + } + want := filepath.Join(home, Dir) + if got != want { + t.Fatalf("DirPath() = %q, want %q", got, want) + } +} + +func TestFilePath(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + got, err := FilePath("context.yaml") + if err != nil { + t.Fatalf("FilePath() error: %v", err) + } + want := filepath.Join(home, Dir, "context.yaml") + if got != want { + t.Fatalf("FilePath() = %q, want %q", got, want) + } +} + +func TestJoinPath(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + got, err := JoinPath("template-cache", "list.json") + if err != nil { + t.Fatalf("JoinPath() error: %v", err) + } + want := filepath.Join(home, Dir, "template-cache", "list.json") + if got != want { + t.Fatalf("JoinPath() = %q, want %q", got, want) + } +} + +func TestFilePathHint(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + got := FilePathHint("context.yaml") + want := filepath.Join(home, Dir, "context.yaml") + if got != want { + t.Fatalf("FilePathHint() = %q, want %q", got, want) + } +} + +func TestFilePathHint_FallsBackToRelPath(t *testing.T) { + t.Setenv("HOME", "") + + got := FilePathHint("context.yaml") + want := FileRelPath("context.yaml") + if got != want { + t.Fatalf("FilePathHint() = %q, want %q", got, want) + } +} + +func TestEnsureDir(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + dir, err := EnsureDir() + if err != nil { + t.Fatalf("EnsureDir() error: %v", err) + } + want := filepath.Join(home, Dir) + if dir != want { + t.Fatalf("EnsureDir() = %q, want %q", dir, want) + } + + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("stat config dir: %v", err) + } + if !info.IsDir() { + t.Fatal("expected directory") + } + if info.Mode().Perm()&0o777 != 0o700 { + t.Fatalf("dir mode = %o, want 0700", info.Mode().Perm()&0o777) + } +} diff --git a/internal/credentials/credentials.go b/internal/credentials/credentials.go index f0b3ca5d..2f9e2dc2 100644 --- a/internal/credentials/credentials.go +++ b/internal/credentials/credentials.go @@ -11,6 +11,8 @@ import ( "github.com/rs/zerolog" "gopkg.in/yaml.v2" + + "github.com/smartcontractkit/cre-cli/internal/creconfig" ) type CreLoginTokenSet struct { @@ -34,7 +36,6 @@ const ( CreApiKeyVar = "CRE_API_KEY" AuthTypeApiKey = "api-key" AuthTypeBearer = "bearer" - ConfigDir = ".cre" ConfigFile = "cre.yaml" // DeploymentAccessStatusFullAccess indicates the organization has full deployment access @@ -61,11 +62,10 @@ func New(logger *zerolog.Logger) (*Credentials, error) { return cfg, nil } - home, err := os.UserHomeDir() + path, err := creconfig.FilePath(ConfigFile) if err != nil { return cfg, nil } - path := filepath.Join(home, ConfigDir, ConfigFile) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("you are not logged in, run cre login and try again") @@ -81,13 +81,9 @@ func New(logger *zerolog.Logger) (*Credentials, error) { } func SaveCredentials(tokenSet *CreLoginTokenSet) error { - home, err := os.UserHomeDir() + dir, err := creconfig.EnsureDir() if err != nil { - return fmt.Errorf("get home dir: %w", err) - } - dir := filepath.Join(home, ConfigDir) - if err := os.MkdirAll(dir, 0o700); err != nil { - return fmt.Errorf("create config dir: %w", err) + return err } path := filepath.Join(dir, ConfigFile) diff --git a/internal/credentials/credentials_test.go b/internal/credentials/credentials_test.go index af594c24..b679c12b 100644 --- a/internal/credentials/credentials_test.go +++ b/internal/credentials/credentials_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/testutil" "github.com/smartcontractkit/cre-cli/internal/testutil/testjwt" ) @@ -42,8 +43,8 @@ func TestNew_WithConfigFile(t *testing.T) { tDir := t.TempDir() t.Setenv("HOME", tDir) - dir := filepath.Join(tDir, ConfigDir) - if err := os.MkdirAll(dir, 0o755); err != nil { + dir, err := creconfig.EnsureDir() + if err != nil { t.Fatalf("failed to create config dir: %v", err) } file := filepath.Join(dir, ConfigFile) diff --git a/internal/telemetry/collector.go b/internal/telemetry/collector.go index 05405eca..1d47dd53 100644 --- a/internal/telemetry/collector.go +++ b/internal/telemetry/collector.go @@ -10,8 +10,12 @@ import ( "github.com/denisbrodbeck/machineid" "github.com/spf13/cobra" "github.com/spf13/pflag" + + "github.com/smartcontractkit/cre-cli/internal/creconfig" ) +const MachineIDFile = "machine_id" + // CollectMachineInfo gathers information about the machine running the CLI func CollectMachineInfo() MachineInfo { return MachineInfo{ @@ -42,10 +46,7 @@ func CollectWorkflowInfo(settings interface{}) *WorkflowInfo { // getOrCreateMachineID retrieves or generates a stable machine ID for telemetry func getOrCreateMachineID() (string, error) { - // Try to read existing machine ID from config (for backwards compatibility) - home, err := os.UserHomeDir() - if err == nil { - idFile := fmt.Sprintf("%s/.cre/machine_id", home) + if idFile, err := creconfig.FilePath(MachineIDFile); err == nil { if data, err := os.ReadFile(idFile); err == nil && len(data) > 0 { return strings.TrimSpace(string(data)), nil } diff --git a/internal/templateconfig/templateconfig.go b/internal/templateconfig/templateconfig.go index e048b752..6b1e49ad 100644 --- a/internal/templateconfig/templateconfig.go +++ b/internal/templateconfig/templateconfig.go @@ -9,13 +9,11 @@ import ( "github.com/rs/zerolog" "gopkg.in/yaml.v3" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" ) -const ( - configDirName = ".cre" - configFileName = "template.yaml" -) +const TemplateConfigFile = "template.yaml" // DefaultSources are the default template repositories. var DefaultSources = []templaterepo.RepoSource{ @@ -31,7 +29,7 @@ var DefaultSources = []templaterepo.RepoSource{ }, } -// Config represents the CLI template configuration file at ~/.cre/template.yaml. +// Config represents the CLI template configuration file (TemplateConfigFile in the config directory). type Config struct { TemplateRepositories []TemplateRepo `yaml:"templateRepositories"` } @@ -43,7 +41,7 @@ type TemplateRepo struct { Ref string `yaml:"ref"` } -// LoadTemplateSources returns the list of template sources from ~/.cre/template.yaml, +// LoadTemplateSources returns template sources from the CLI config file, // falling back to the default source if the file doesn't exist. func LoadTemplateSources(logger *zerolog.Logger) []templaterepo.RepoSource { cfg, err := loadConfigFile(logger) @@ -62,16 +60,11 @@ func LoadTemplateSources(logger *zerolog.Logger) []templaterepo.RepoSource { return DefaultSources } -// SaveTemplateSources writes the given sources to ~/.cre/template.yaml. +// SaveTemplateSources writes the given sources to the CLI template config file. func SaveTemplateSources(sources []templaterepo.RepoSource) error { - homeDir, err := os.UserHomeDir() + dir, err := creconfig.EnsureDir() if err != nil { - return fmt.Errorf("get home directory: %w", err) - } - - dir := filepath.Join(homeDir, configDirName) - if err := os.MkdirAll(dir, 0750); err != nil { - return fmt.Errorf("create config directory: %w", err) + return err } var repos []TemplateRepo @@ -89,7 +82,7 @@ func SaveTemplateSources(sources []templaterepo.RepoSource) error { return fmt.Errorf("marshal config: %w", err) } - configPath := filepath.Join(dir, configFileName) + configPath := filepath.Join(dir, TemplateConfigFile) tmp := configPath + ".tmp" if err := os.WriteFile(tmp, data, 0600); err != nil { return fmt.Errorf("write temp file: %w", err) @@ -102,15 +95,13 @@ func SaveTemplateSources(sources []templaterepo.RepoSource) error { return nil } -// EnsureDefaultConfig creates ~/.cre/template.yaml with the default source +// EnsureDefaultConfig creates the CLI template config file with the default source // if the file does not already exist. func EnsureDefaultConfig(logger *zerolog.Logger) error { - homeDir, err := os.UserHomeDir() + configPath, err := creconfig.FilePath(TemplateConfigFile) if err != nil { - return fmt.Errorf("get home directory: %w", err) + return err } - - configPath := filepath.Join(homeDir, configDirName, configFileName) if _, err := os.Stat(configPath); err == nil { return nil // file already exists } @@ -143,12 +134,10 @@ func ParseRepoString(s string) (templaterepo.RepoSource, error) { } func loadConfigFile(logger *zerolog.Logger) (*Config, error) { - homeDir, err := os.UserHomeDir() + configPath, err := creconfig.FilePath(TemplateConfigFile) if err != nil { return nil, err } - - configPath := filepath.Join(homeDir, configDirName, configFileName) data, err := os.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { diff --git a/internal/templateconfig/templateconfig_test.go b/internal/templateconfig/templateconfig_test.go index 7ef4d947..2b5a61c4 100644 --- a/internal/templateconfig/templateconfig_test.go +++ b/internal/templateconfig/templateconfig_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" "github.com/smartcontractkit/cre-cli/internal/testutil" ) @@ -59,7 +60,8 @@ func TestLoadTemplateSourcesFromConfigFile(t *testing.T) { homeDir := t.TempDir() t.Setenv("HOME", homeDir) - configDir := filepath.Join(homeDir, ".cre") + configDir, err := creconfig.DirPath() + require.NoError(t, err) require.NoError(t, os.MkdirAll(configDir, 0750)) configContent := `templateRepositories: @@ -68,7 +70,7 @@ func TestLoadTemplateSourcesFromConfigFile(t *testing.T) { ref: release ` require.NoError(t, os.WriteFile( - filepath.Join(configDir, "template.yaml"), + filepath.Join(configDir, TemplateConfigFile), []byte(configContent), 0600, )) @@ -94,8 +96,9 @@ func TestSaveTemplateSources(t *testing.T) { require.NoError(t, SaveTemplateSources(sources)) // Verify file exists - configPath := filepath.Join(homeDir, ".cre", "template.yaml") - _, err := os.Stat(configPath) + configPath, err := creconfig.FilePath(TemplateConfigFile) + require.NoError(t, err) + _, err = os.Stat(configPath) require.NoError(t, err) // Verify content by loading back diff --git a/internal/templaterepo/cache.go b/internal/templaterepo/cache.go index 0640cd8a..bd79ade1 100644 --- a/internal/templaterepo/cache.go +++ b/internal/templaterepo/cache.go @@ -9,16 +9,17 @@ import ( "time" "github.com/rs/zerolog" + + "github.com/smartcontractkit/cre-cli/internal/creconfig" ) const ( templateListCacheDuration = 1 * time.Hour tarballCacheDuration = 24 * time.Hour - cacheDirName = "template-cache" - creDirName = ".cre" + TemplateCacheDir = "template-cache" ) -// Cache manages template list and tarball caching at ~/.cre/template-cache/. +// Cache manages template list and tarball caching under the CLI config directory. type Cache struct { logger *zerolog.Logger cacheDir string @@ -33,12 +34,10 @@ type templateListCache struct { // NewCache creates a new Cache instance. func NewCache(logger *zerolog.Logger) (*Cache, error) { - homeDir, err := os.UserHomeDir() + cacheDir, err := creconfig.JoinPath(TemplateCacheDir) if err != nil { - return nil, fmt.Errorf("failed to get home directory: %w", err) + return nil, fmt.Errorf("failed to get cache directory: %w", err) } - - cacheDir := filepath.Join(homeDir, creDirName, cacheDirName) if err := os.MkdirAll(cacheDir, 0750); err != nil { return nil, fmt.Errorf("failed to create cache directory: %w", err) } diff --git a/internal/tenantctx/tenantctx.go b/internal/tenantctx/tenantctx.go index ec3f2064..4a8434c7 100644 --- a/internal/tenantctx/tenantctx.go +++ b/internal/tenantctx/tenantctx.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/environments" ) @@ -90,7 +91,7 @@ const getTenantConfigQuery = `query GetTenantConfig { }` // FetchAndWriteContext fetches the user context from the service -// and writes the registry manifest to ~/.cre/. +// and writes the registry manifest to the CLI config directory. func FetchAndWriteContext(ctx context.Context, gqlClient *graphqlclient.Client, envName string, log *zerolog.Logger) error { ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() @@ -208,14 +209,14 @@ func parseChainSelectorJSON(raw []byte) (uint64, error) { return 0, fmt.Errorf("chain selector must be a decimal string or integer JSON value: %s", string(raw)) } -// LoadContext reads the registry manifest from ~/.cre/ +// LoadContext reads the registry manifest from the CLI config directory // and returns the EnvironmentContext for the given environment name. func LoadContext(envName string) (*EnvironmentContext, error) { - home, err := os.UserHomeDir() + path, err := creconfig.FilePath(ContextFile) if err != nil { - return nil, fmt.Errorf("get home dir: %w", err) + return nil, err } - return LoadContextFromPath(filepath.Join(home, credentials.ConfigDir, ContextFile), envName) + return LoadContextFromPath(path, envName) } // LoadContextFromPath reads the registry manifest at the given path @@ -263,14 +264,9 @@ func EnsureContext(ctx context.Context, creds *credentials.Credentials, envSet * } func writeContextFile(data map[string]*EnvironmentContext, log *zerolog.Logger) error { - home, err := os.UserHomeDir() + dir, err := creconfig.EnsureDir() if err != nil { - return fmt.Errorf("get home dir: %w", err) - } - - dir := filepath.Join(home, credentials.ConfigDir) - if err := os.MkdirAll(dir, 0o700); err != nil { - return fmt.Errorf("create config dir: %w", err) + return err } out, err := yaml.Marshal(data) diff --git a/internal/update/update.go b/internal/update/update.go index d61e0dd1..3dd50ce1 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -12,15 +12,16 @@ import ( "github.com/Masterminds/semver/v3" "github.com/rs/zerolog" + + "github.com/smartcontractkit/cre-cli/internal/creconfig" ) const ( - githubAPIURL = "https://api.github.com/repos/smartcontractkit/cre-cli/releases/latest" - repoURL = "https://github.com/smartcontractkit/cre-cli/releases" - timeout = 6 * time.Second - cacheDuration = 24 * time.Hour - cacheFileName = "update.json" - cacheDirName = ".cre" + githubAPIURL = "https://api.github.com/repos/smartcontractkit/cre-cli/releases/latest" + repoURL = "https://github.com/smartcontractkit/cre-cli/releases" + timeout = 6 * time.Second + cacheDuration = 24 * time.Hour + UpdateCacheFile = "update.json" ) // githubRelease is a minimal struct to parse the JSON response @@ -36,12 +37,11 @@ type cacheState struct { } func getCachePath(logger *zerolog.Logger) (string, error) { - homeDir, err := os.UserHomeDir() + path, err := creconfig.FilePath(UpdateCacheFile) if err != nil { - logger.Debug().Msgf("Failed to get user home directory: %v", err) - return "", err + logger.Debug().Msgf("Failed to get update cache path: %v", err) } - return filepath.Join(homeDir, cacheDirName, cacheFileName), nil + return path, err } func loadCache(path string, logger *zerolog.Logger) (*cacheState, error) { diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 92720350..f2161f4e 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -20,6 +20,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/authvalidation" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/creconfig" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/settings" @@ -55,13 +56,14 @@ func mockGetCreOrganizationInfoGraphQLPayload() map[string]any { } } -// CreateTestBearerCredentialsHome writes JWT bearer credentials under HOME/.cre for subprocess CLI tests. +// CreateTestBearerCredentialsHome writes JWT bearer credentials under the CLI config directory for subprocess CLI tests. func CreateTestBearerCredentialsHome(t *testing.T) string { t.Helper() homeDir := t.TempDir() - creDir := filepath.Join(homeDir, ".cre") - require.NoError(t, os.MkdirAll(creDir, 0o700), "failed to create .cre dir") + t.Setenv("HOME", homeDir) + creDir, err := creconfig.EnsureDir() + require.NoError(t, err, "failed to create config dir") jwt := createTestJWT("test-org-id") creConfig := "AccessToken: " + jwt + "\n" + @@ -70,7 +72,7 @@ func CreateTestBearerCredentialsHome(t *testing.T) string { "ExpiresIn: 3600\n" + "TokenType: Bearer\n" - require.NoError(t, os.WriteFile(filepath.Join(creDir, "cre.yaml"), []byte(creConfig), 0o600), "failed to write test credentials") + require.NoError(t, os.WriteFile(filepath.Join(creDir, credentials.ConfigFile), []byte(creConfig), 0o600), "failed to write test credentials") return homeDir } From dd4ec8c209fa8221e12c332d608a54f5c4a4c103 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 15:06:27 +0100 Subject: [PATCH 4/7] Lint --- cmd/logout/logout.go | 2 +- cmd/logout/logout_test.go | 2 +- cmd/secrets/common/browser_flow.go | 2 +- cmd/workflow/hash/hash.go | 2 +- internal/tenantctx/tenantctx.go | 2 +- internal/update/update.go | 10 +++++----- test/multi_command_flows/workflow_private_registry.go | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/logout/logout.go b/cmd/logout/logout.go index d274202f..5b005e39 100644 --- a/cmd/logout/logout.go +++ b/cmd/logout/logout.go @@ -10,8 +10,8 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/internal/constants" - "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/creconfig" + "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/tenantctx" diff --git a/cmd/logout/logout_test.go b/cmd/logout/logout_test.go index 59899fbb..3f5f4432 100644 --- a/cmd/logout/logout_test.go +++ b/cmd/logout/logout_test.go @@ -9,8 +9,8 @@ import ( "gopkg.in/yaml.v3" - "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/creconfig" + "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/testutil" diff --git a/cmd/secrets/common/browser_flow.go b/cmd/secrets/common/browser_flow.go index cb626cac..f98fc166 100644 --- a/cmd/secrets/common/browser_flow.go +++ b/cmd/secrets/common/browser_flow.go @@ -224,7 +224,7 @@ func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method s }) var exchangeResp struct { ExchangeAuthCodeToToken struct { - AccessToken string `json:"accessToken"` + AccessToken string `json:"accessToken"` // #nosec G117 -- matches OAuth token exchange response field ExpiresIn int `json:"expiresIn"` } `json:"exchangeAuthCodeToToken"` } diff --git a/cmd/workflow/hash/hash.go b/cmd/workflow/hash/hash.go index 7efdb434..2f7fa09e 100644 --- a/cmd/workflow/hash/hash.go +++ b/cmd/workflow/hash/hash.go @@ -24,7 +24,7 @@ type Inputs struct { WorkflowName string WorkflowPath string OwnerFromSettings string - PrivateKey string + PrivateKey string // #nosec G117 -- CLI flag for optional signing key input SkipTypeChecks bool RegistryType settings.RegistryType DerivedOwner string diff --git a/internal/tenantctx/tenantctx.go b/internal/tenantctx/tenantctx.go index 4a8434c7..530613a1 100644 --- a/internal/tenantctx/tenantctx.go +++ b/internal/tenantctx/tenantctx.go @@ -15,8 +15,8 @@ import ( "gopkg.in/yaml.v2" "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" - "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/creconfig" + "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" ) diff --git a/internal/update/update.go b/internal/update/update.go index 3dd50ce1..4e92e542 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -17,11 +17,11 @@ import ( ) const ( - githubAPIURL = "https://api.github.com/repos/smartcontractkit/cre-cli/releases/latest" - repoURL = "https://github.com/smartcontractkit/cre-cli/releases" - timeout = 6 * time.Second - cacheDuration = 24 * time.Hour - UpdateCacheFile = "update.json" + githubAPIURL = "https://api.github.com/repos/smartcontractkit/cre-cli/releases/latest" + repoURL = "https://github.com/smartcontractkit/cre-cli/releases" + timeout = 6 * time.Second + cacheDuration = 24 * time.Hour + UpdateCacheFile = "update.json" ) // githubRelease is a minimal struct to parse the JSON response diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index f2161f4e..07498cba 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -19,8 +19,8 @@ import ( "github.com/smartcontractkit/cre-cli/internal/authvalidation" "github.com/smartcontractkit/cre-cli/internal/constants" - "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/creconfig" + "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/settings" From aeefc78a5fb207c5d285e693dbdee217e27e6ea5 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 15:35:48 +0100 Subject: [PATCH 5/7] Fix test --- internal/logger/logger_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 97207091..4141b398 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -60,8 +60,9 @@ func TestLogger(t *testing.T) { log.Info().Msg("pretty message") output := buf.String() - // Pretty logging typically includes colors (ANSI escape codes) - assert.Contains(t, output, "\x1b[") + // ConsoleWriter uses human-readable format instead of JSON (colors are omitted without a TTY). + assert.Contains(t, output, "INF pretty message") + assert.NotContains(t, output, `"level"`) }) t.Run("Logger with fields", func(t *testing.T) { From c9cfaeb41a6a2682b1cfaa60452638c4310bd81a Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Tue, 2 Jun 2026 16:05:34 +0100 Subject: [PATCH 6/7] Fix test --- internal/logger/logger_test.go | 5 +- .../workflow_private_registry.go | 146 +++++++----------- 2 files changed, 63 insertions(+), 88 deletions(-) diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 4141b398..da38138d 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -60,8 +60,9 @@ func TestLogger(t *testing.T) { log.Info().Msg("pretty message") output := buf.String() - // ConsoleWriter uses human-readable format instead of JSON (colors are omitted without a TTY). - assert.Contains(t, output, "INF pretty message") + // ConsoleWriter uses human-readable format instead of JSON; ANSI colors depend on TTY detection. + assert.Contains(t, output, "pretty message") + assert.Contains(t, output, "INF") assert.NotContains(t, output, `"level"`) }) diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 07498cba..b2fb7a47 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -61,9 +61,8 @@ func CreateTestBearerCredentialsHome(t *testing.T) string { t.Helper() homeDir := t.TempDir() - t.Setenv("HOME", homeDir) - creDir, err := creconfig.EnsureDir() - require.NoError(t, err, "failed to create config dir") + creDir := filepath.Join(homeDir, creconfig.Dir) + require.NoError(t, os.MkdirAll(creDir, 0o700), "failed to create config dir") jwt := createTestJWT("test-org-id") creConfig := "AccessToken: " + jwt + "\n" + @@ -77,6 +76,59 @@ func CreateTestBearerCredentialsHome(t *testing.T) string { return homeDir } +// realGoCacheEnv returns GOPATH and GOMODCACHE locations outside t.TempDir()-backed HOME dirs. +// Overriding HOME makes Go default GOPATH to $HOME/go; module files are read-only and break TempDir cleanup. +func realGoCacheEnv(t *testing.T) (gopath, gomodcache string) { + t.Helper() + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + gopath = os.Getenv("GOPATH") + if gopath == "" { + gopath = filepath.Join(realHome, "go") + } + + gomodcache = os.Getenv("GOMODCACHE") + if gomodcache == "" { + gomodcache = filepath.Join(gopath, "pkg", "mod") + } + + return gopath, gomodcache +} + +// pinGoCacheForTestHome keeps module cache out of temp HOME directories in the test process. +func pinGoCacheForTestHome(t *testing.T) { + t.Helper() + gopath, gomodcache := realGoCacheEnv(t) + t.Setenv("GOPATH", gopath) + t.Setenv("GOMODCACHE", gomodcache) +} + +// cliChildEnv builds subprocess env with isolated HOME for credentials and pinned Go cache paths. +func cliChildEnv(t *testing.T, testHome string) []string { + t.Helper() + gopath, gomodcache := realGoCacheEnv(t) + + childEnv := make([]string, 0, len(os.Environ())+4) + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || + strings.HasPrefix(entry, "USERPROFILE=") || + strings.HasPrefix(entry, "GOPATH=") || + strings.HasPrefix(entry, "GOMODCACHE=") { + continue + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, + "HOME="+testHome, + "USERPROFILE="+testHome, + "GOPATH="+gopath, + "GOMODCACHE="+gomodcache, + ) + return childEnv +} + func createTestJWT(orgID string) string { return testjwt.CreateTestJWT(orgID) } @@ -241,29 +293,7 @@ func workflowDeployPrivateRegistry(t *testing.T, tc TestConfig) string { cmd := exec.Command(CLIPath, args...) testHome := CreateTestBearerCredentialsHome(t) - - realHome, err := os.UserHomeDir() - require.NoError(t, err, "failed to get real home dir") - - childEnv := make([]string, 0, len(os.Environ())+3) - hasGOPATH := false - for _, entry := range os.Environ() { - if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { - continue - } - if strings.HasPrefix(entry, "GOPATH=") { - hasGOPATH = true - } - childEnv = append(childEnv, entry) - } - childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) - // When HOME is overridden, Go defaults GOPATH to $HOME/go which lands - // inside t.TempDir(). Go modules are read-only, so TempDir cleanup - // fails and marks the test as failed. Pin GOPATH to the real home. - if !hasGOPATH { - childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) - } - cmd.Env = childEnv + cmd.Env = cliChildEnv(t, testHome) var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr @@ -418,26 +448,7 @@ func workflowPausePrivateRegistry(t *testing.T, tc TestConfig) string { cmd := exec.Command(CLIPath, args...) testHome := CreateTestBearerCredentialsHome(t) - - realHome, err := os.UserHomeDir() - require.NoError(t, err, "failed to get real home dir") - - childEnv := make([]string, 0, len(os.Environ())+3) - hasGOPATH := false - for _, entry := range os.Environ() { - if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { - continue - } - if strings.HasPrefix(entry, "GOPATH=") { - hasGOPATH = true - } - childEnv = append(childEnv, entry) - } - childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) - if !hasGOPATH { - childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) - } - cmd.Env = childEnv + cmd.Env = cliChildEnv(t, testHome) var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr @@ -588,26 +599,7 @@ func workflowActivatePrivateRegistry(t *testing.T, tc TestConfig) string { cmd := exec.Command(CLIPath, args...) testHome := CreateTestBearerCredentialsHome(t) - - realHome, err := os.UserHomeDir() - require.NoError(t, err, "failed to get real home dir") - - childEnv := make([]string, 0, len(os.Environ())+3) - hasGOPATH := false - for _, entry := range os.Environ() { - if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { - continue - } - if strings.HasPrefix(entry, "GOPATH=") { - hasGOPATH = true - } - childEnv = append(childEnv, entry) - } - childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) - if !hasGOPATH { - childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) - } - cmd.Env = childEnv + cmd.Env = cliChildEnv(t, testHome) var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr @@ -746,26 +738,7 @@ func workflowDeletePrivateRegistry(t *testing.T, tc TestConfig) string { cmd := exec.Command(CLIPath, args...) testHome := CreateTestBearerCredentialsHome(t) - - realHome, err := os.UserHomeDir() - require.NoError(t, err, "failed to get real home dir") - - childEnv := make([]string, 0, len(os.Environ())+3) - hasGOPATH := false - for _, entry := range os.Environ() { - if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { - continue - } - if strings.HasPrefix(entry, "GOPATH=") { - hasGOPATH = true - } - childEnv = append(childEnv, entry) - } - childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) - if !hasGOPATH { - childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) - } - cmd.Env = childEnv + cmd.Env = cliChildEnv(t, testHome) var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr @@ -857,6 +830,7 @@ func RunPrivateRegistryAuthAndSettingsFinalize(t *testing.T, envPath, blankWorkf bearerHome := CreateTestBearerCredentialsHome(t) t.Setenv("HOME", bearerHome) t.Setenv("USERPROFILE", bearerHome) + pinGoCacheForTestHome(t) logger := testutil.NewTestLogger() creds, err := credentials.New(logger) From 6d52873aa585dba793f7c7f11c79f5d0bc3e7b03 Mon Sep 17 00:00:00 2001 From: timothyF95 Date: Wed, 3 Jun 2026 12:50:11 +0100 Subject: [PATCH 7/7] feedback --- cmd/templates/add/add.go | 2 +- cmd/templates/remove/remove.go | 2 +- docs/cre_templates_add.md | 2 +- docs/cre_templates_remove.md | 2 +- internal/creconfig/creconfig.go | 10 ++-------- internal/creconfig/creconfig_test.go | 10 +--------- 6 files changed, 7 insertions(+), 21 deletions(-) diff --git a/cmd/templates/add/add.go b/cmd/templates/add/add.go index b1d09e29..1abc0720 100644 --- a/cmd/templates/add/add.go +++ b/cmd/templates/add/add.go @@ -21,7 +21,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return &cobra.Command{ Use: "add ...", Short: "Adds a template repository source", - Long: fmt.Sprintf("Adds one or more template repository sources to %s. These repositories are used by cre init to discover available templates.", creconfig.FileRelPath(templateconfig.TemplateConfigFile)), + Long: fmt.Sprintf("Adds one or more template repository sources to your home directory (%s/%s). These repositories are used by cre init to discover available templates.", creconfig.Dir, templateconfig.TemplateConfigFile), Args: cobra.MinimumNArgs(1), Example: "cre templates add smartcontractkit/cre-templates@main myorg/my-templates", RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/templates/remove/remove.go b/cmd/templates/remove/remove.go index 762298c9..c8b0e996 100644 --- a/cmd/templates/remove/remove.go +++ b/cmd/templates/remove/remove.go @@ -21,7 +21,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return &cobra.Command{ Use: "remove ...", Short: "Removes a template repository source", - Long: fmt.Sprintf("Removes one or more template repository sources from %s. The ref portion is optional and ignored during matching.", creconfig.FileRelPath(templateconfig.TemplateConfigFile)), + Long: fmt.Sprintf("Removes one or more template repository sources from your home directory (%s/%s). The ref portion is optional and ignored during matching.", creconfig.Dir, templateconfig.TemplateConfigFile), Args: cobra.MinimumNArgs(1), Example: "cre templates remove smartcontractkit/cre-templates myorg/my-templates", RunE: func(cmd *cobra.Command, args []string) error { diff --git a/docs/cre_templates_add.md b/docs/cre_templates_add.md index 74933c5d..3af5ce5c 100644 --- a/docs/cre_templates_add.md +++ b/docs/cre_templates_add.md @@ -4,7 +4,7 @@ Adds a template repository source ### Synopsis -Adds one or more template repository sources to .cre/template.yaml. These repositories are used by cre init to discover available templates. +Adds one or more template repository sources to your home directory (.cre/template.yaml). These repositories are used by cre init to discover available templates. ``` cre templates add ... [flags] diff --git a/docs/cre_templates_remove.md b/docs/cre_templates_remove.md index 5f1afa5e..d17742f6 100644 --- a/docs/cre_templates_remove.md +++ b/docs/cre_templates_remove.md @@ -4,7 +4,7 @@ Removes a template repository source ### Synopsis -Removes one or more template repository sources from .cre/template.yaml. The ref portion is optional and ignored during matching. +Removes one or more template repository sources from your home directory (.cre/template.yaml). The ref portion is optional and ignored during matching. ``` cre templates remove ... [optional flags] diff --git a/internal/creconfig/creconfig.go b/internal/creconfig/creconfig.go index 7ca27316..b9bd846c 100644 --- a/internal/creconfig/creconfig.go +++ b/internal/creconfig/creconfig.go @@ -38,19 +38,13 @@ func FilePath(name string) (string, error) { return filepath.Join(dir, name), nil } -// FileRelPath returns a home-relative path for static help text and committed docs. -// It uses the host OS path separator (e.g. ".cre/template.yaml" or ".cre\template.yaml"). -func FileRelPath(name string) string { - return filepath.Join(Dir, name) -} - // FilePathHint returns the absolute config file path for user-facing messages, -// or a home-relative path if the home directory cannot be resolved. +// or a doc-style path (Dir/name) if the home directory cannot be resolved. func FilePathHint(name string) string { if path, err := FilePath(name); err == nil { return path } - return FileRelPath(name) + return filepath.Join(Dir, name) } // JoinPath returns an absolute path under the CLI config directory. diff --git a/internal/creconfig/creconfig_test.go b/internal/creconfig/creconfig_test.go index 5bcf7cc4..adfc5ec1 100644 --- a/internal/creconfig/creconfig_test.go +++ b/internal/creconfig/creconfig_test.go @@ -6,14 +6,6 @@ import ( "testing" ) -func TestFileRelPath(t *testing.T) { - got := FileRelPath("context.yaml") - want := filepath.Join(Dir, "context.yaml") - if got != want { - t.Fatalf("FileRelPath() = %q, want %q", got, want) - } -} - func TestDirPath(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) @@ -71,7 +63,7 @@ func TestFilePathHint_FallsBackToRelPath(t *testing.T) { t.Setenv("HOME", "") got := FilePathHint("context.yaml") - want := FileRelPath("context.yaml") + want := filepath.Join(Dir, "context.yaml") if got != want { t.Fatalf("FilePathHint() = %q, want %q", got, want) }